Skip to content

LEG-126: fix label updates in Linear MCP update and create actions#3

Open
sjawhar wants to merge 1 commit into
prime-radiant-inc:mainfrom
sjawhar:LEG-126
Open

LEG-126: fix label updates in Linear MCP update and create actions#3
sjawhar wants to merge 1 commit into
prime-radiant-inc:mainfrom
sjawhar:LEG-126

Conversation

@sjawhar

@sjawhar sjawhar commented Feb 15, 2026

Copy link
Copy Markdown

Summary

Fixes silent failure when using labels parameter in update and create actions. The MCP accepted a labels array but never processed it, always returning "No updates provided" when labels were the only update.

Changes

  • Add getWorkspaceLabels() cached fetcher and resolveLabels() name-to-ID resolver (follows existing getTeams()/resolveState() pattern)
  • Fix handleUpdate() to resolve label names to IDs and include labelIds in the mutation
  • Fix handleCreate() similarly
  • Update help text and tool description to document label support
  • Label cache refreshes on miss to handle recently created labels
  • Unknown labels return actionable error with the specific missing names and available options

Verification

  • labels=["worker-active"] → sets label (instead of "No updates provided")
  • labels=[] → clears all labels
  • labels=["nonexistent"] → error with missing name and available labels
  • Combined state="Done" + labels=[...] → both update
  • Build passes: npm run build

Implements LEG-126

Summary by CodeRabbit

  • New Features

    • Added label support for creating and updating issues; labels are now displayed in issue details.
    • New public APIs to fetch and resolve workspace labels, including caching and missing-name reporting.
    • Label changes are reflected in update diffs returned by the system.
  • Documentation

    • Updated help text and usage examples to demonstrate label workflows.

@coderabbitai

coderabbitai Bot commented Feb 15, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds workspace label support to the Linear integration: new label fetch/cache and resolution functions, label integration into create/update/get flows, and updated help text and examples. Public API exports include getWorkspaceLabels and resolveLabels.

Changes

Cohort / File(s) Summary
Label support & core flows
mcp/src/linear-core.ts
Added getWorkspaceLabels() (fetch + cache) and resolveLabels(names) (name→ID mapping with fallback). Integrated label resolution into handleCreate(), handleUpdate() (resolving or returning missing names), and included labels in handleGet() outputs. Exported new functions.
Documentation/help snippets
mcp/src/linear-core.ts (help text blocks)
Updated help/documentation strings to show label usage examples for create/update operations.
Stylistic/formatting tweaks
mcp/src/linear-core.ts
Minor formatting and chaining adjustments (GraphQL call formatting, array/map chains) without behavioral changes beyond label additions.

Sequence Diagram(s)

sequenceDiagram
  actor Client
  participant LinearCore as Linear Core
  participant LabelCache as WorkspaceLabelCache
  participant LinearAPI as Linear GraphQL API

  Client->>LinearCore: create/update/get with label names
  LinearCore->>LabelCache: getWorkspaceLabels()?
  alt cache miss
    LabelCache->>LinearAPI: GraphQL query for workspace labels
    LinearAPI-->>LabelCache: labels list (id, name)
    LabelCache-->>LinearCore: cached labels
  else cache hit
    LabelCache-->>LinearCore: cached labels
  end
  LinearCore->>LinearCore: resolveLabels(names) -> ids or missing
  alt creating/updating with resolved ids
    LinearCore->>LinearAPI: GraphQL mutation (include label IDs)
    LinearAPI-->>LinearCore: mutation result
    LinearCore-->>Client: created/updated issue (includes labels)
  else missing label names
    LinearCore-->>Client: error/report of missing label names
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I hopped through code to fetch each name,

mapping colors, tags, and tiny fame.
Labels cached, resolved with care,
now issues wear their florals fair.
— a rabbit celebrates the patch with flair 🌿

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically identifies the main change: fixing label updates in Linear MCP update and create actions, directly addressing the core functionality added in this PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sjawhar

sjawhar commented Feb 15, 2026

Copy link
Copy Markdown
Author

Review Summary

CRITICAL (P1): 0 issues
IMPORTANT (P2): 2 observations
MINOR (P3): 2 suggestions

Verdict: Approved to merge — no blocking issues.


What This Does

Fixes the silent failure when labels is passed to update or create actions. The MCP accepted a labels array in its schema but never processed it in handleUpdate() or handleCreate(), so labels was the only field → empty input → "No updates provided".

Key Findings

Architecture (✅ Good)

  • getWorkspaceLabels() / resolveLabels() follows existing getTeams() / resolveState() pattern exactly
  • Discriminated union return ({ ids } | { missing }) is clean — callers check "missing" in result
  • Refresh-on-miss for stale label cache is a nice touch for recently created labels
  • dispatchAction() already wires params.labels through to both handlers
  • LinearParams zod schema already had labels: z.array(z.string()).optional() — only handler logic was missing

Correctness (✅ Verified against acceptance criteria)

  • labels=["worker-active"] → resolves name to ID, sends labelIds in mutation
  • labels=[]resolveLabels short-circuits to { ids: [] }, mutation clears labels
  • labels=["nonexistent"] → returns actionable error with specific missing name + available labels
  • Combined state + labels → both processed into input, single mutation
  • Response includes labels { nodes { name } } for confirmation in change tracking

P2: 250 label pagination limitissueLabels(first: 250) won't paginate. If a workspace exceeds 250 labels, some won't resolve. This matches existing patterns (teams don't paginate either) and is unlikely to hit in practice, but worth noting for future hardening.

P2: Formatter noise in diff — ~40% of the diff is cosmetic reformatting (parenthesized as casts, graphql template literal indentation). Not wrong, but makes the functional changes harder to isolate in review. Consider running formatter as a separate commit in future PRs.

P3: Unrelated lockfile changespackage-lock.json removes peer: true from express and zod entries, likely from an npm install side effect.

Comment thread mcp/src/linear-core.ts Outdated
export async function getWorkspaceLabels(): Promise<Array<{ id: string; name: string }>> {
if (cachedLabels) return cachedLabels;
const data = (await graphql(`
query { issueLabels(first: 250) { nodes { id name } } }

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2 — Observation]: first: 250 without pagination. If a workspace accumulates 250+ labels, resolveLabels will fail to find labels beyond that limit and return a "not found" error even for valid names. Matches existing patterns (teams fetch is also unpaginated), so not blocking — but worth a // TODO: paginate if needed comment for future awareness.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added // TODO: paginate if >250 labels comment. Also applied CodeRabbit's dedup suggestion ([...new Set(ids)]) in resolveLabels return.

Comment thread mcp/package-lock.json
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
"peer": true,
"dependencies": {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3 — Nit]: Unrelated lockfile changes — peer: true removed from express and zod entries. Probably an npm install side effect. Not harmful, just noise in the diff.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged. The lockfile changed again during conflict resolution rebuild — same npm install side effect. No functional impact.

@sjawhar sjawhar marked this pull request as ready for review February 15, 2026 05:17
Resolve label names to IDs via new getWorkspaceLabels()/resolveLabels() functions.
handleUpdate() and handleCreate() now process the labels parameter instead of
silently dropping it. Empty arrays clear labels, unknown labels return actionable
errors. Label cache refreshes on miss to handle recently created labels.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mcp/src/linear-core.ts`:
- Around line 130-132: The GraphQL query string uses JavaScript-style "//"
comments which Linear's parser will reject; in the getWorkspaceLabels() call
where graphql(`query { issueLabels(first: 250) { nodes { id name } } } // TODO:
paginate if >250 labels`), replace the inline "//" comment with a GraphQL "#"
comment (or remove it) so the query becomes valid GraphQL (e.g., append " #
TODO: paginate if >250 labels" or just remove the comment) and ensure the
graphql(...) invocation and the data typing remain unchanged.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1914c06 and f8b8cd5.

⛔ Files ignored due to path filters (3)
  • mcp/dist/cli.js is excluded by !**/dist/**
  • mcp/dist/index.js is excluded by !**/dist/**
  • mcp/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (1)
  • mcp/src/linear-core.ts

Comment thread mcp/src/linear-core.ts
Comment on lines +130 to +132
const data = (await graphql(`
query { issueLabels(first: 250) { nodes { id name } } } // TODO: paginate if >250 labels
`)) as { issueLabels: { nodes: Array<{ id: string; name: string }> } };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cd mcp/src && head -n 140 linear-core.ts | tail -n 15 && echo "---" && sed -n '125,135p' linear-core.ts

Repository: obra/streamlinear

Length of output: 1176


🌐 Web query:

GraphQL comment syntax # vs // valid comments

💡 Result:

In standard GraphQL syntax (queries and SDL), the only valid comment form is single-line comments starting with #:

# this is a comment
query { me { id } } # trailing comment

The GraphQL spec defines comments as starting with # and continuing to the end of the line; comments are otherwise ignored like whitespace. [1]

// is not a valid GraphQL comment delimiter per the spec, so using it will generally produce a parse error unless a particular tool adds a non-standard extension. [1]

Also note: """ ... """ is not a “comment”; in SDL it’s a description string (often shown in schema docs/introspection), not ignored like # comments. [2]

Sources:
[1] GraphQL Specification (Working Draft) — Comment grammar uses # only https://spec.graphql.org/draft/
[2] GraphQL.com Learn — notes # comments vs documentation/descriptions https://graphql.com/learn/schema/


🏁 Script executed:

rg -n "export.*function graphql|const graphql.*=" mcp/src/linear-core.ts

Repository: obra/streamlinear

Length of output: 94


🏁 Script executed:

# Search for graphql helper definition in the codebase
rg -n "function graphql\|const graphql" --type ts -A 10 | head -50

Repository: obra/streamlinear

Length of output: 43


🏁 Script executed:

sed -n '19,50p' mcp/src/linear-core.ts

Repository: obra/streamlinear

Length of output: 964


Replace // with # for GraphQL comment syntax.

Line 131 uses JavaScript-style // comment syntax inside the GraphQL query string. GraphQL only supports # comments, so this will cause a parse error when sent to the Linear API, breaking getWorkspaceLabels() at runtime.

Suggested fix
 export async function getWorkspaceLabels(): Promise<Array<{ id: string; name: string }>> {
   if (cachedLabels) return cachedLabels;
   const data = (await graphql(`
+    # TODO: paginate if >250 labels
     query { issueLabels(first: 250) { nodes { id name } } }
   `)) as { issueLabels: { nodes: Array<{ id: string; name: string }> } };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const data = (await graphql(`
query { issueLabels(first: 250) { nodes { id name } } } // TODO: paginate if >250 labels
`)) as { issueLabels: { nodes: Array<{ id: string; name: string }> } };
export async function getWorkspaceLabels(): Promise<Array<{ id: string; name: string }>> {
if (cachedLabels) return cachedLabels;
const data = (await graphql(`
# TODO: paginate if >250 labels
query { issueLabels(first: 250) { nodes { id name } } }
`)) as { issueLabels: { nodes: Array<{ id: string; name: string }> } };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcp/src/linear-core.ts` around lines 130 - 132, The GraphQL query string uses
JavaScript-style "//" comments which Linear's parser will reject; in the
getWorkspaceLabels() call where graphql(`query { issueLabels(first: 250) { nodes
{ id name } } } // TODO: paginate if >250 labels`), replace the inline "//"
comment with a GraphQL "#" comment (or remove it) so the query becomes valid
GraphQL (e.g., append " # TODO: paginate if >250 labels" or just remove the
comment) and ensure the graphql(...) invocation and the data typing remain
unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant