Skip to content

Add initial Linear custom field tools#4

Merged
itz4blitz merged 2 commits intomainfrom
feat/custom-field-tools
Apr 18, 2026
Merged

Add initial Linear custom field tools#4
itz4blitz merged 2 commits intomainfrom
feat/custom-field-tools

Conversation

@itz4blitz
Copy link
Copy Markdown
Owner

Summary

  • add linear_getCustomFields, linear_getIssueCustomFields, and linear_updateIssueCustomField with direct GraphQL schema discovery against the authenticated Linear client
  • normalize custom field definitions and issue values without assuming string-only data, and fail fast when the schema does not expose a compatible value slot
  • wire the new tools through definitions, handlers, registration, Smithery metadata, docs, and focused tests for JSON value passthrough, null clearing, and validation

Testing

  • npm test
  • npm run build

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 LinearService for 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.

Comment thread src/services/linear-service.ts Outdated

const pending = this.requestGraphQL<GraphQLTypeQueryResult>(INTROSPECT_TYPE_QUERY, {
name: typeName,
}).then((result) => result.__type);
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
}).then((result) => result.__type);
})
.then((result) => result.__type)
.catch((error) => {
this.typeCache.delete(typeName);
throw error;
});

Copilot uses AI. Check for mistakes.
Comment thread src/services/linear-service.ts Outdated
Comment on lines +1604 to +1611
const selectionSet = await this.buildSelectionSet(plan.mutationField.type, [
'success',
'issue',
'customFieldValue',
'fieldValue',
'customField',
'value',
]);
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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
}
`;

Copilot uses AI. Check for mistakes.
Comment on lines +1556 to +1574
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}
}
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread src/tools/type-guards.ts Outdated
/**
* Type guard for linear_getCustomFields tool arguments
*/
export function isGetCustomFieldsArgs(args: unknown): args is Record<string, never> {
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
export function isGetCustomFieldsArgs(args: unknown): args is Record<string, never> {
export function isGetCustomFieldsArgs(
args: unknown
): args is Record<string, never> | undefined {

Copilot uses AI. Check for mistakes.
@itz4blitz
Copy link
Copy Markdown
Owner Author

@copilot apply changes based on the comments in this thread

Copy link
Copy Markdown

Copilot AI commented Apr 18, 2026

@copilot apply changes based on the comments in this thread

Implemented all requested review-thread fixes in commit 961cb42.

This includes:

  • clearing rejected introspection promises from the type cache so retries can succeed
  • improving custom-field update selection hydration for currentValue by using nested field preferences for custom field value nodes
  • fixing isGetCustomFieldsArgs type guard narrowing for undefined
  • adding focused tests for custom-field discovery happy paths and schema-missing failures (plus a transient introspection retry case)

No UI changes were made in this PR.

@itz4blitz itz4blitz merged commit 6d15b43 into main Apr 18, 2026
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.

3 participants