Conversation
There was a problem hiding this comment.
Pull request overview
Adds initial support for Linear custom fields by introducing three new MCP tools backed by dynamic GraphQL schema introspection on the authenticated Linear client.
Changes:
- Added new tools:
linear_getCustomFields,linear_getIssueCustomFields,linear_updateIssueCustomField(definitions, type-guards, handlers, registration). - Implemented schema-discovery + normalization logic in
LinearServicefor custom field definitions, issue values, and updates via a discovered mutation. - Updated docs/metadata and added focused Jest tests for update JSON passthrough, null clearing, and value-slot validation.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/tools/type-guards.ts | Adds JSON-compatible value guarding and new tool argument validators. |
| src/tools/handlers/issue-handlers.ts | Adds handlers for the three custom-field tools. |
| src/tools/handlers/index.ts | Registers and exports the new custom-field handlers. |
| src/tools/definitions/issue-tools.ts | Adds MCP tool definitions + JSON-ish schemas for custom fields. |
| src/tools/definitions/index.ts | Wires new tool definitions into the exported list. |
| src/services/linear-service.ts | Implements GraphQL introspection, selection-set building, normalization, and mutation execution for custom fields. |
| src/tests/linear-service.test.ts | Adds tests for updateIssueCustomField JSON passthrough / null clearing / mismatch failure. |
| package.json | Adds new tools to Smithery metadata tooling list. |
| TOOLS.md | Documents new custom-field tools and updates their status. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| const pending = this.requestGraphQL<GraphQLTypeQueryResult>(INTROSPECT_TYPE_QUERY, { | ||
| name: typeName, | ||
| }).then((result) => result.__type); |
There was a problem hiding this comment.
getType caches the pending introspection promise, but if the request rejects (transient network/auth issues) the rejected promise will remain in typeCache and permanently poison future calls for that type. Consider clearing the cache entry on rejection (e.g., pending.catch(() => this.typeCache.delete(typeName))) so a later retry can succeed.
| }).then((result) => result.__type); | |
| }) | |
| .then((result) => result.__type) | |
| .catch((error) => { | |
| this.typeCache.delete(typeName); | |
| throw error; | |
| }); |
| const selectionSet = await this.buildSelectionSet(plan.mutationField.type, [ | ||
| 'success', | ||
| 'issue', | ||
| 'customFieldValue', | ||
| 'fieldValue', | ||
| 'customField', | ||
| 'value', | ||
| ]); |
There was a problem hiding this comment.
updateIssueCustomField builds a selection set from the mutation payload type using buildSelectionSet(...), but buildSelectionSet uses DEFAULT_NESTED_FIELDS for nested object fields. That means nested payload fields like customFieldValue/fieldValue are unlikely to include value/displayValue/etc, so currentValue can come back missing the data implied by the tool’s output schema. Consider building explicit nested selections (or enhancing buildSelectionSet to accept per-field preferred selections) so the returned currentValue is fully populated.
| const selectionSet = await this.buildSelectionSet(plan.mutationField.type, [ | |
| 'success', | |
| 'issue', | |
| 'customFieldValue', | |
| 'fieldValue', | |
| 'customField', | |
| 'value', | |
| ]); | |
| const currentValueSelectionSet = ` | |
| { | |
| ${ISSUE_CUSTOM_FIELD_VALUE_FIELDS.join('\n ')} | |
| } | |
| `; | |
| const selectionSet = ` | |
| { | |
| success | |
| issue { | |
| id | |
| } | |
| customFieldValue ${currentValueSelectionSet} | |
| fieldValue ${currentValueSelectionSet} | |
| issueCustomField ${currentValueSelectionSet} | |
| customField { | |
| id | |
| } | |
| value | |
| } | |
| `; |
| async getCustomFields() { | ||
| const field = await this.resolveCustomFieldDefinitionsField(); | ||
| const selectionSet = await this.buildSelectionSet(field.type, CUSTOM_FIELD_DEFINITION_FIELDS); | ||
| const query = `query LinearGetCustomFields { ${field.name}${selectionSet} }`; | ||
| const response = await this.requestGraphQL<Record<string, unknown>>(query); | ||
|
|
||
| return extractListItems(response[field.name]).map((item) => normalizeCustomFieldDefinition(item)); | ||
| } | ||
|
|
||
| async getIssueCustomFields(issueId: string) { | ||
| const field = await this.resolveIssueCustomFieldValuesField(); | ||
| const selectionSet = await this.buildSelectionSet(field.type, ISSUE_CUSTOM_FIELD_VALUE_FIELDS); | ||
| const query = ` | ||
| query LinearGetIssueCustomFields($id: String!) { | ||
| issue(id: $id) { | ||
| id | ||
| identifier | ||
| ${field.name}${selectionSet} | ||
| } |
There was a problem hiding this comment.
New getCustomFields / getIssueCustomFields logic relies on schema discovery + dynamic selection-set generation, but there are currently no unit tests covering these methods (unlike createIssue/updateIssue/createComment). Adding tests for at least the happy path and the “schema field not found” failure path would help prevent regressions in the discovery heuristics.
| /** | ||
| * Type guard for linear_getCustomFields tool arguments | ||
| */ | ||
| export function isGetCustomFieldsArgs(args: unknown): args is Record<string, never> { |
There was a problem hiding this comment.
isGetCustomFieldsArgs is declared as a type guard for Record<string, never>, but it returns true when args === undefined. That makes the type predicate unsound (TS can narrow args to an object even though it may be undefined). Either stop accepting undefined here, or change the predicate to args is Record<string, never> | undefined (or a dedicated NoArgs type) to keep the narrowing accurate.
| export function isGetCustomFieldsArgs(args: unknown): args is Record<string, never> { | |
| export function isGetCustomFieldsArgs( | |
| args: unknown | |
| ): args is Record<string, never> | undefined { |
|
@copilot apply changes based on the comments in this thread |
Agent-Logs-Url: https://github.com/itz4blitz/mcp-linear/sessions/9e00524e-342d-4547-aaed-ef6d6a8c620f Co-authored-by: itz4blitz <60281870+itz4blitz@users.noreply.github.com>
Implemented all requested review-thread fixes in commit This includes:
No UI changes were made in this PR. |
Summary
linear_getCustomFields,linear_getIssueCustomFields, andlinear_updateIssueCustomFieldwith direct GraphQL schema discovery against the authenticated Linear clientTesting