Skip to content

Add project milestone CRUD tools#5

Merged
itz4blitz merged 6 commits intomainfrom
feat/milestone-tools
Apr 18, 2026
Merged

Add project milestone CRUD tools#5
itz4blitz merged 6 commits intomainfrom
feat/milestone-tools

Conversation

@itz4blitz
Copy link
Copy Markdown
Owner

Summary

  • add the first milestone CRUD slice with service methods, handlers, tool definitions, type guards, and registration for linear_getMilestones, linear_getMilestoneById, linear_createMilestone, linear_updateMilestone, and linear_archiveMilestone
  • normalize milestone payloads around Linear's ProjectMilestone SDK model and keep milestone-project association helpers out of scope for this first pass
  • add focused milestone tests, update package metadata, and document the new milestone tools in TOOLS.md

Validation

  • 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 first-pass CRUD support for Linear project milestones to the MCP server, wiring new tool definitions through handlers into LinearService, plus tests and documentation updates.

Changes:

  • Introduces milestone tool definitions + handler registration for list/get/create/update/archive operations.
  • Implements milestone service methods with a normalizer based on Linear’s ProjectMilestone SDK model.
  • Adds milestone-focused tests, bumps package version, and documents the new tools in TOOLS.md.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/tools/type-guards.ts Adds type guards for milestone tool arguments.
src/tools/handlers/milestone-handlers.ts New milestone tool handlers that validate args and delegate to LinearService.
src/tools/handlers/index.ts Registers milestone handlers with the tool router and re-exports them.
src/tools/definitions/milestone-tools.ts New MCP tool definitions for milestone CRUD operations.
src/tools/definitions/index.ts Adds milestone tool definitions to the global tool list/exports.
src/services/linear-service.ts Implements milestone list/get/create/update/archive methods and normalization.
src/tests/milestone-tools.test.ts Verifies milestone tool registration, routing, and argument validation behavior.
src/tests/linear-service.test.ts Adds sanitization/update-requirement tests for milestone create/update payloads.
package.json Bumps version and adds milestone tools to Smithery tool list.
TOOLS.md Documents milestone tools and removes them from the “planned” list.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/tools/type-guards.ts Outdated
Comment on lines +664 to +668
typeof args === 'object' &&
args !== null &&
(!('includeArchived' in args) ||
typeof (args as { includeArchived: boolean }).includeArchived === 'boolean') &&
(!('limit' in args) || typeof (args as { limit: number }).limit === 'number')
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.

isGetMilestonesArgs accepts any number for limit (including NaN, Infinity, negatives, and non-integers) and also treats arrays as valid objects. Since limit is passed to Linear pagination (first), this can allow invalid inputs past the type guard and trigger runtime errors. Consider requiring a plain object (non-array) and validating limit as a finite positive integer (you already have isJsonObject/isPositiveInteger helpers later in this file).

Suggested change
typeof args === 'object' &&
args !== null &&
(!('includeArchived' in args) ||
typeof (args as { includeArchived: boolean }).includeArchived === 'boolean') &&
(!('limit' in args) || typeof (args as { limit: number }).limit === 'number')
isJsonObject(args) &&
(!('includeArchived' in args) ||
typeof (args as { includeArchived: boolean }).includeArchived === 'boolean') &&
(!('limit' in args) || isPositiveInteger((args as { limit: number }).limit))

Copilot uses AI. Check for mistakes.
default: false,
},
limit: {
type: 'number',
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.

linear_getMilestones defines limit as { type: 'number' }, but this value is used as a pagination first argument which is an integer in Linear/GraphQL. Defining this as type: 'integer' with minimum: 1 (similar to view-tools.ts) would prevent invalid non-integer/negative limits from being advertised/accepted.

Suggested change
type: 'number',
type: 'integer',
minimum: 1,

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +43
output_schema: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
description: { type: 'string' },
status: { type: 'string' },
progress: { type: 'number' },
sortOrder: { type: 'number' },
targetDate: { type: 'string' },
createdAt: { type: 'string' },
updatedAt: { type: 'string' },
archivedAt: { type: 'string' },
project: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
},
},
},
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.

The milestone output_schema marks several fields as always string/object, but the service normalizer can emit null for project (and Linear SDK fields like description, targetDate, archivedAt are commonly nullable). This makes the documented schema inaccurate and can break any downstream schema-based consumers. Consider updating these properties to allow null where applicable (e.g., type: ['string','null'] / type: ['object','null']) and applying the same pattern consistently across the milestone tool outputs.

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 updates in 312f84b: tightened linear_getMilestones arg guard to plain-object + positive-integer limit, changed milestone limit schema to integer with minimum: 1, and updated milestone output schemas to allow nullable fields (description, targetDate, archivedAt, project) with added tests covering these behaviors.

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

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

description: 'Updated sort order for the milestone within its project',
},
},
required: ['id'],
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.

linear_updateMilestone currently accepts { id } only in the tool schema, but LinearService.updateMilestone will throw At least one milestone field must be provided when no updatable fields are present. Consider encoding this constraint in input_schema (e.g., an anyOf requiring one of name, projectId, description, targetDate, or sortOrder, similar to linear_updateSavedView) so invalid calls are rejected by schema validation before reaching the service.

Suggested change
required: ['id'],
required: ['id'],
anyOf: [
{ required: ['name'] },
{ required: ['projectId'] },
{ required: ['description'] },
{ required: ['targetDate'] },
{ required: ['sortOrder'] },
],

Copilot uses AI. Check for mistakes.
Comment thread src/tools/type-guards.ts Outdated
(!('projectId' in args) || typeof (args as { projectId: string }).projectId === 'string') &&
(!('description' in args) || typeof (args as { description: string }).description === 'string') &&
(!('targetDate' in args) || typeof (args as { targetDate: string }).targetDate === 'string') &&
(!('sortOrder' in args) || typeof (args as { sortOrder: number }).sortOrder === 'number')
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.

isUpdateMilestoneArgs returns true for { id: string } even though LinearService.updateMilestone rejects updates with no fields to change. Consider tightening the type guard to require at least one of name, projectId, description, targetDate, or sortOrder (mirroring the service check) so callers get a consistent Invalid arguments for updateMilestone error instead of a later service-level error.

Suggested change
(!('sortOrder' in args) || typeof (args as { sortOrder: number }).sortOrder === 'number')
(!('sortOrder' in args) || typeof (args as { sortOrder: number }).sortOrder === 'number') &&
('name' in args ||
'projectId' in args ||
'description' in args ||
'targetDate' in args ||
'sortOrder' in args)

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

Applied in e7da114: linear_updateMilestone now requires at least one mutable field in input_schema via anyOf, and isUpdateMilestoneArgs now rejects { id } without update fields. Added tests for both schema and id-only argument rejection. No UI changes (screenshot not applicable).

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

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Comment thread src/tools/type-guards.ts
Comment on lines +659 to +668
export function isGetMilestonesArgs(args: unknown): args is {
includeArchived?: boolean;
limit?: number;
} {
return (
isJsonObject(args) &&
(!('includeArchived' in args) ||
typeof (args as { includeArchived: boolean }).includeArchived === 'boolean') &&
(!('limit' in args) || isPositiveInteger((args as { limit: number }).limit))
);
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.

isGetMilestonesArgs rejects null, but linear_getMilestones has no required args and the MCP server will pass null when arguments is omitted. Either allow null here (treat as {}) or ensure the handler coerces null to {} before calling this guard.

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +22
export function handleGetMilestones(linearService: LinearService) {
return async (args: unknown) => {
try {
if (!isGetMilestonesArgs(args)) {
throw new Error('Invalid arguments for getMilestones');
}

return await linearService.getMilestones(args);
} catch (error) {
logError('Error getting milestones', error);
throw error;
}
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.

linear_getMilestones has only optional fields, but MCP server passes args = null when the caller omits arguments (see src/mcp-server.ts:70). With the current guard, null is rejected, so calling the tool without args will throw even though the schema has defaults. Consider treating null as {} in the handler (or updating the type guard to accept null for this tool) and add a test that handlers.linear_getMilestones(null) succeeds.

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

@copilot apply changes based on the comments in this thread

Agent-Logs-Url: https://github.com/itz4blitz/mcp-linear/sessions/0bca453f-1e50-4bee-8292-e2ea05a6872b

Co-authored-by: itz4blitz <60281870+itz4blitz@users.noreply.github.com>
@itz4blitz itz4blitz requested a review from Copilot April 18, 2026 22:37
Copy link
Copy Markdown

Copilot AI commented Apr 18, 2026

@copilot apply changes based on the comments in this thread

Implemented in dabda83: linear_getMilestones now normalizes args with args ?? {} before validation/service call, so omitted MCP arguments (null) succeeds instead of throwing. Added test coverage asserting handlers.linear_getMilestones(null) resolves and calls the service with {}. No UI changes (screenshot not applicable).

@itz4blitz
Copy link
Copy Markdown
Owner Author

@copilot resolve the merge conflicts in this pull request

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

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Copy link
Copy Markdown

Copilot AI commented Apr 18, 2026

@copilot resolve the merge conflicts in this pull request

Resolved in 7bc2664 by merging origin/main into this branch and fixing all conflicts in src/services/linear-service.ts, src/tools/definitions/index.ts, src/tools/handlers/index.ts, and src/tools/type-guards.ts, preserving both roadmap and milestone functionality. No UI changes (screenshot not applicable).

# Conflicts:
#	src/services/linear-service.ts
#	src/tools/definitions/index.ts
#	src/tools/handlers/index.ts
#	src/tools/type-guards.ts
@itz4blitz itz4blitz merged commit 53efc7f into main Apr 18, 2026
@itz4blitz itz4blitz deleted the feat/milestone-tools branch April 18, 2026 22:53
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