Skip to main content
Glama
d0lim
by d0lim

linear-eye

A read-only MCP server for Linear. It stores current snapshots and observed changes in Cloudflare D1, then answers questions about current work, project and milestone progress, member activity, and weekly reports.

Linear remains the source of truth. The server does not modify Linear data or call an LLM.

Architecture

Linear Webhook ─ HMAC + timestamp ─ Queue ─┐
                                         ├─ Effect services ─ Effect SQL / D1
Linear GraphQL ─ full sync / daily Cron ──┘                         │
                                                                  ▼
MCP client ─ Bearer ─ createMcpHandler ─ Effect intelligence ────── D1

One Cloudflare Worker provides fetch, queue, and scheduled handlers. Cloudflare owns durable retries and scheduling; Effect manages application logic within each invocation.

  • Effect 4.0.0-rc.118 and @effect/sql-d1 4.0.0-rc.118, pinned together. Services use Context.Service, Layer, Effect Schema, and typed errors.

  • Parameterized SQL through the Effect D1 driver, without an ORM. Writes use D1's atomic batch rather than BEGIN transactions.

  • Stateless MCP transport through agents/createMcpHandler and MCP SDK v2. Zod describes the transport schemas; Effect Schema validates application inputs.

  • Database, configuration, Queue, and Linear client dependencies are injected through Layers. Analytics services do not access the Linear client or Worker bindings.

  • No individual scores, rankings, or productivity assessments.

See the MVP specification, Effect architecture decisions, and verification record.

Related MCP server: Roster

Prerequisites

  • Node.js 22 or newer and pnpm 10 for development and builds. The deployed application runs on Workers.

  • A Cloudflare account with Workers, D1, and Queues access.

  • A read-only API key for one Linear workspace and permission to create webhooks.

  • A Streamable HTTP MCP client that supports a custom Bearer authorization header.

Local development

git clone https://github.com/d0lim/linear-eye.git
cd linear-eye
pnpm install --frozen-lockfile
cp .dev.vars.example .dev.vars
# Replace the four placeholder values in .dev.vars with development credentials.
pnpm db:migrate:local
pnpm dev

.dev.vars is excluded from Git. Linear requires a publicly reachable HTTPS URL for webhook delivery, so localhost alone cannot receive real deliveries. The local Queue consumer stops when the development server exits.

curl http://localhost:8787/health
pnpm check
pnpm test
pnpm build

Tests run in the Workers runtime with real local D1. They cover migrations, atomic rollback, duplicate and out-of-order webhooks, sync pagination and recovery, analytics, and MCP HTTP requests. Tests require no Linear or Cloudflare account. pnpm build performs a Wrangler deployment dry run; it does not deploy.

Create D1 and Queues

pnpm exec wrangler login
pnpm exec wrangler d1 create linear-eye
pnpm exec wrangler queues create linear-eye-events
pnpm exec wrangler queues create linear-eye-dead-letter

Replace the placeholder UUID in wrangler.jsonc with the database_id returned by D1 creation before running a remote migration or deployment. Keep the binding names DB and LINEAR_EYE_QUEUE.

pnpm db:migrate:remote

This applies migrations/0001_initial.sql to the new database. Use new migrations for later schema changes; do not edit a migration already applied in production.

Configure secrets and deploy

Create a read-only API key in Linear Settings → API. Generate separate, long random tokens for MCP and admin access; for example, run openssl rand -hex 32 once for each token.

pnpm exec wrangler secret put LINEAR_API_KEY
pnpm exec wrangler secret put LINEAR_WEBHOOK_SECRET
pnpm exec wrangler secret put MCP_AUTH_TOKEN
pnpm exec wrangler secret put ADMIN_AUTH_TOKEN
pnpm deploy

Copy the webhook signing secret from the webhook details in Linear. For a new installation without a webhook yet, deploy the Worker first, create the webhook using the next section, and then register its signing secret. Requests return 401 until the secret is configured. If a test delivery failed during setup, check that the webhook is active after registering the secret.

Keep production secrets out of wrangler.jsonc, source files, and commits. Non-secret settings are:

Variable

Default

Purpose

REPORT_TIMEZONE

Asia/Seoul

IANA timezone for date ranges and weekly reports

STALE_ISSUE_DAYS

5

Days without an observed change before an in-progress issue is considered stale

PROJECT_UPDATE_BODY_LIMIT

8000

Maximum stored ProjectUpdate body length in characters, capped at 8000

Reports use English headings. Set REPORT_TIMEZONE to the timezone your team uses; report language does not change date boundaries.

Configure the Linear webhook

In Linear Settings → API → Webhooks, register:

https://<worker>/webhooks/linear

Subscribe to Issue, Project, ProjectUpdate, and User events. Select the workspace and team scope accessible to the API key. Do not subscribe to comments.

The endpoint validates HMAC-SHA256 over the raw request bytes using Linear-Signature. It requires Linear-Delivery and the millisecond Linear-Timestamp header, and checks that both the header timestamp and the signed body's webhookTimestamp are within 60 seconds. The HTTP handler projects the payload, enqueues it, and returns 200 without accessing D1.

Safe values from unknown changed fields are retained in history. Queue messages are limited to 96 KiB in UTF-8, with at most 64 KiB for unknown-field history. Oversized values receive an explicit $linearEyeTruncated marker. If field names cannot fit, the projection records the number omitted. Large extensions therefore do not discard known state or title changes. Excluded content, including descriptions and comments, is removed at every nesting level.

Initial sync and status

Enable the webhook before starting bootstrap so changes made during bootstrap can also be observed.

export WORKER_URL='https://<worker>'
# Set ADMIN_AUTH_TOKEN to your separately stored admin token.
curl -X POST "$WORKER_URL/admin/sync" \
  -H "Authorization: Bearer $ADMIN_AUTH_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"mode":"full"}'

The endpoint returns HTTP 202 with { "accepted": true, "runId": "..." }. The sync runs through the Queue after the HTTP response.

curl "$WORKER_URL/admin/sync/<runId>" \
  -H "Authorization: Bearer $ADMIN_AUTH_TOKEN"

Wait for status: "completed". The response includes pagesProcessed, entitiesProcessed, and error. MCP tools return SYNC_NOT_READY until the first full sync completes.

Full sync order: users → teams → workflow states → projects → project milestones → issues → project updates. Each Queue message processes one GraphQL page of up to 50 entities. A page receipt and its continuation are committed together, allowing retries to recover an enqueue failure after the database commit. Version checks prevent older sync pages or webhooks from overwriting newer snapshots.

Reconciliation and recovery

Cron starts incremental reconciliation every day at 18:00 UTC / 03:00 Asia/Seoul. It covers users, workflow states, projects, project milestones, and issues. The query watermark is the start of the last successful run minus a five-minute overlap. Using the start time avoids skipping changes made while a run was in progress.

curl -X POST "$WORKER_URL/admin/reconcile" \
  -H "Authorization: Bearer $ADMIN_AUTH_TOKEN" \
  -H 'Content-Type: application/json' -d '{}'

Linear page requests have a 20-second deadline covering both headers and response-body consumption. Expiration is a retryable error. The Queue retries failures up to five times, respects Retry-After on rate limits, and sends exhausted messages to linear-eye-dead-letter. Terminal sync failures are also recorded in the status API. Once the cause is resolved, start a new sync through the admin endpoint.

Replay webhook dead-letter messages to the original Queue using Cloudflare tooling, preserving their bodies and order. The Linear-Delivery receipt prevents duplicate persistence. During retries, sync status remains running. If a Worker cannot execute at all, it cannot update that status, so also inspect Queue backlog and dead-letter messages. Recover before Queue retention expires to preserve event history.

pnpm exec wrangler tail

Structured JSON logs cover webhook reception, rejection, processing and duplicates; sync starts, completion and failures; GraphQL pages and errors; and MCP tool names and latency. Secrets, full user questions, issue descriptions, and complete project-update bodies are not logged.

Connect an MCP client

  • URL: https://<worker>/mcp

  • Transport: Streamable HTTP

  • Header: Authorization: Bearer <MCP_AUTH_TOKEN>

Use the MCP token, not the admin token. OAuth discovery and login are not implemented, so clients must support custom Bearer headers. Browser CORS is not enabled.

MCP protocol smoke request:

curl "$WORKER_URL/mcp" \
  -H "Authorization: Bearer $MCP_AUTH_TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2025-03-26' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

Available tools

Tool

Example arguments

get_team_current_work

{ "team": "SHOP", "includeStale": true }

get_member_activity

{ "member": "alice@example.com", "from": "2026-09-21", "to": "2026-09-27" }

get_project_progress

{ "project": "Storefront" }

get_milestone_progress

{ "project": "Storefront", "milestone": "Checkout" }

get_changes

{ "from": "2026-09-21", "to": "2026-09-27", "issue": "SHOP-123", "fields": ["state"], "limit": 100 }

get_weekly_report

{ "member": "alice@example.com", "week": "current" }

  • Members resolve by exact ID, then email, display name, and name. Projects and milestones also require exact matches. Ambiguous matches return candidates and an AMBIGUOUS_* error.

  • Dates are calendar dates in REPORT_TIMEZONE, inclusive at both ends. Weeks start on Monday. Weekly reports also accept week: "previous" or a Monday weekStart, such as "2026-09-21".

  • Every report includes tracking coverage. A range starting before tracking began has complete: false.

  • Count and estimate progress exclude archived, deleted, and canceled issues from the denominator. Canceled issues have a separate count. An empty denominator returns null; missing estimates are not assigned an implicit value of one.

  • The actor and the assignee at the time of a change are distinct. If assignment history is unavailable, attribution falls back to the current snapshot and sets inferred: true.

  • get_changes returns up to 500 changes at a time, in chronological order. When truncated is true, pass nextCursor as cursor with the same filters to retrieve the next page.

  • Weekly currentlyInProgress reflects the current snapshot; check currentlyInProgressAsOf. The Markdown draft uses a deterministic English template, without LLM-generated prose.

  • includeStale: false excludes work with no recent observed changes. Staleness is an activity fact, not a performance assessment.

Known limitations

  • One workspace, read-only. No OAuth, dashboard, Slack/GitHub integration, LLM calls, or issue-description/comment ingestion.

  • Bootstrap does not backfill historical activity. Reconciliation repairs snapshots without inventing missed intermediate events. Coverage describes the tracking window, not proof that every webhook arrived.

  • Issue change reports use field_changes. Creation and removal events, and events for other entities, are stored, but the MVP has no separate event-timeline tool.

  • Names and workflow state types use current metadata. Reports do not fully reproduce names as they appeared before a rename.

  • Incremental queries cannot detect entities that have disappeared entirely from the API. A lost removal webhook requires operational investigation. Archived entities are recovered with includeArchived: true.

  • Effect 4 is a release candidate. Upgrade Effect and its D1 driver together and run the full test suite.

  • Page, query, and body sizes are bounded, but CPU, daily reads/writes/requests, Queue operations, and storage limits need validation for each workspace. Event retention and automatic deletion are not implemented.

  • Real workspace bootstrap and hosted MCP access require account credentials and operational verification. Local tests and dry runs do not establish production readiness; see the verification record for runtime coverage and remaining checks.

Contributing

See CONTRIBUTING.md for development checks and contribution guidelines. This repository contains a deployable Worker application; private: true in package.json prevents accidental npm publication and does not make the GitHub repository private.

License

MIT.

API references

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Provides sub-millisecond access to organizational and engineering data by pre-materializing data into a read-only SQLite database. Enables agents to query people, teams, issues, features, and governance documents without upstream latency or auth.
    2
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables querying your team's work (stats, overdue, workload, active members, etc.) via natural language, scoped to your department with read-only signed-token access.
    39 npm
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    A read-only MCP server that gives LLM agents deterministic operational facts about Trello boards, including structural history, card movement, workflow flow, staleness, and due-date information.
    14
    MIT