Conversation
Add CustomView-backed saved view operations and favorite view support so local MCP clients can manage and validate Linear views against the current API schema.
There was a problem hiding this comment.
Pull request overview
This PR adds MCP tool support for Linear “saved views” (CustomView) and “favorite views”, wiring up tool definitions, handlers, and LinearService methods, plus associated validation and tests.
Changes:
- Added saved view & favorite view MCP tool definitions and registered their handlers.
- Implemented
LinearServicemethods for listing/creating/updating/deleting saved views and querying favorites via a custom GraphQL query. - Added unit tests for the new type guards and service behavior; updated Smithery tool list and TOOLS.md docs.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/tools/type-guards.ts | Adds validation/type guards for saved view & favorite view tool inputs. |
| src/tools/handlers/view-handlers.ts | New handlers delegating to LinearService for saved/favorite view tools. |
| src/tools/handlers/index.ts | Registers and re-exports the new view handlers. |
| src/tools/definitions/view-tools.ts | New tool schemas for saved views & favorite views. |
| src/tools/definitions/index.ts | Adds view tool definitions to the exported/registered list. |
| src/services/linear-service.ts | Implements CustomView CRUD + favorites GraphQL query + normalization helpers. |
| src/tests/view-type-guards.test.ts | Adds coverage for the new view-related type guards. |
| src/tests/linear-service.test.ts | Adds coverage for saved view sanitization, pagination forwarding, and favorites filtering. |
| package.json | Adds the new tools to the Smithery manifest tool list. |
| TOOLS.md | Documents the new “Views and Filters” tool set and removes them from “Planned”. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export function isGetFavoriteViewsArgs(args: unknown): args is { | ||
| limit?: number; | ||
| includeArchived?: boolean; | ||
| orderBy?: string; | ||
| } { | ||
| return ( | ||
| typeof args === 'object' && | ||
| args !== null && | ||
| (!('limit' in args) || isPositiveInteger((args as { limit: unknown }).limit)) && | ||
| (!('includeArchived' in args) || | ||
| typeof (args as { includeArchived: boolean }).includeArchived === 'boolean') && | ||
| (!('orderBy' in args) || isSavedViewOrderBy((args as { orderBy: unknown }).orderBy)) | ||
| ); |
There was a problem hiding this comment.
isGetFavoriteViewsArgs has the same issue as isGetSavedViewsArgs: arrays like [] will pass because only typeof args === 'object' is checked. Consider validating with isJsonObject(args) / !Array.isArray(args) and narrowing the type predicate for orderBy to 'createdAt' | 'updatedAt' since other values are rejected.
| typeof (args as { initiativeId: string }).initiativeId === 'string' && | ||
| 'projectId' in args && | ||
| typeof (args as { projectId: string }).projectId === 'string' | ||
| typeof (args as { projectId: string }).projectId === 'string' |
There was a problem hiding this comment.
There appears to be a formatting/indentation regression in this guard: the typeof (args as { projectId: string }).projectId === 'string' line is indented as if it were nested under nothing, making the condition block harder to read and potentially failing lint/prettier checks. Reformatting this expression to match surrounding guards would improve consistency.
| typeof (args as { projectId: string }).projectId === 'string' | |
| typeof (args as { projectId: string }).projectId === 'string' |
| properties: { | ||
| id: { type: 'string' }, | ||
| name: { type: 'string' }, | ||
| slugId: { type: 'string' }, |
There was a problem hiding this comment.
In the linear_getFavoriteViews output schema, customView.slugId is declared as { type: 'string' }, but the service-side type for favorites models it as slugId?: string | null and the handler forwards whatever comes back from GraphQL. To avoid schema/tooling mismatches when slugId is missing/null, consider making this field nullable (or at least optional+nullable) in the output schema (e.g., align it with nullableStringSchema).
| slugId: { type: 'string' }, | |
| slugId: nullableStringSchema, |
| description: 'Updated project filter data for the saved view. Pass null to clear it.', | ||
| }, | ||
| }, | ||
| required: ['id'], |
There was a problem hiding this comment.
linear_updateSavedView rejects calls that only provide id (both in the type guard and in LinearService.updateSavedView), but the JSON schema only requires id and doesn't express the "at least one other field" requirement. If consumers rely on the schema for client-side validation, they may construct requests that always fail at runtime. Consider adding a schema constraint such as minProperties: 2 or an anyOf requiring one mutable field alongside id.
| required: ['id'], | |
| required: ['id'], | |
| anyOf: [ | |
| { required: ['name'] }, | |
| { required: ['description'] }, | |
| { required: ['shared'] }, | |
| { required: ['icon'] }, | |
| { required: ['color'] }, | |
| { required: ['teamId'] }, | |
| { required: ['projectId'] }, | |
| { required: ['ownerId'] }, | |
| { required: ['filters'] }, | |
| { required: ['filterData'] }, | |
| { required: ['projectFilterData'] }, | |
| ], |
| export function isGetSavedViewsArgs(args: unknown): args is { | ||
| limit?: number; | ||
| includeArchived?: boolean; | ||
| orderBy?: string; | ||
| } { | ||
| return ( | ||
| typeof args === 'object' && | ||
| args !== null && | ||
| (!('limit' in args) || isPositiveInteger((args as { limit: unknown }).limit)) && | ||
| (!('includeArchived' in args) || | ||
| typeof (args as { includeArchived: boolean }).includeArchived === 'boolean') && | ||
| (!('orderBy' in args) || isSavedViewOrderBy((args as { orderBy: unknown }).orderBy)) | ||
| ); | ||
| } |
There was a problem hiding this comment.
isGetSavedViewsArgs accepts arrays because it only checks typeof args === 'object' / args !== null. For example, [] will pass validation and be treated as an empty options object. Consider reusing isJsonObject(args) (or adding !Array.isArray(args)) so only plain objects are accepted. Also, since the guard only allows 'createdAt' | 'updatedAt', the type predicate should narrow orderBy to that union instead of string to preserve type safety downstream.
|
@copilot apply changes based on the comments in this thread |
Agent-Logs-Url: https://github.com/itz4blitz/mcp-linear/sessions/de6b5ea9-534e-4fb4-b2f2-ecf3ea160431 Co-authored-by: itz4blitz <60281870+itz4blitz@users.noreply.github.com>
Implemented the requested review-thread fixes in commit |
Summary
CustomViewAPI, including definitions, handlers, registration, and Smithery manifest updatesfavorites()query mismatch by issuing a minimal supported favorites query, with unit coverage plus live smoke tests against a real workspaceTesting
linear_getSavedViewslinear_getFavoriteViews