linear-toon-mcp 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4548c28ca6deb74cc3ea72d889aad5f931ed4d63265f9d742f8ad54b1af651b9
4
- data.tar.gz: 8089aab524d5febf2207243b5de33524dc2a264fdc8624a009d1ed3fbf4ce9a0
3
+ metadata.gz: 2c8f7853b331d24b32b67d2f6f5db8fae9c4833a25470011019463c69c621fac
4
+ data.tar.gz: '058de398a53a8109347de0ac55f75d6407a2a65e07cbdd10cd8a65754ae99009'
5
5
  SHA512:
6
- metadata.gz: 64044b9580eef34aab49067d721ee0435b38658a6bd99ba5594260457b1424beb4f9f3236de429986a6f099c2c60debfa9a72635538ac095f91f5440b343adf0
7
- data.tar.gz: 2f679acce91253b8632c913c3be49bb0329aa8a2eb84538205a24ef248fcf7e0448b9ff0b8263087cbc39a993c23cf383f3868868ca94d7691b3c695051ebfbc
6
+ metadata.gz: bd93c8d45aa51d44ae10b3e83adebc94db4f09588c73483797163b7ea8ee9ee8b7659383b450beab5759449e99c2788899c97430acd466dc16f5ee25adfbd129
7
+ data.tar.gz: 417404d0048ec42ae75cd1d11f445d283ac3bc72f2bfa265f8825e47e7d1da0746ff579236755285efbaf0ea1288340c390d9e6f41df31834059e6522c222464
data/README.md CHANGED
@@ -42,7 +42,7 @@ claude mcp add linear-toon -e LINEAR_API_KEY=lin_api_xxxxx -- linear-toon-mcp
42
42
 
43
43
  ## Tools
44
44
 
45
- Most parameters accept either a UUID or a human identifier (issue identifier like `LIN-123`, team key like `VIB`, project/initiative/label name, user email or `"me"`); the server resolves it. `save_*` tools dispatch by `id` presence — provide an `id` to update, omit it to create. All responses are TOON-encoded.
45
+ Most parameters accept either a UUID or a human identifier (issue identifier like `LIN-123`, team key like `VIB`, project/initiative/label name, user email or `"me"`); the server resolves it, and rejects blank values. `save_*` tools dispatch by `id` presence — provide an `id` to update, omit it to create. All responses are TOON-encoded.
46
46
 
47
47
  ### Issues
48
48
 
@@ -68,6 +68,8 @@ Most parameters accept either a UUID or a human identifier (issue identifier lik
68
68
  | `get_project` | Retrieve a project by name, ID, or slug. Optional includes for members, milestones, and resources. |
69
69
  | `save_project` | Create or update a project. On create, `name` and `teams` (one or more) are required. Resolves names to IDs for teams, lead, members, labels, status, and initiative (initiative links on creation only — use `add_project_to_initiative` afterwards). |
70
70
  | `archive_project` | Archive a project (recoverable soft delete). Linear has no hard-delete for projects. |
71
+ | `save_project_milestone` | Create or update a project milestone. On create, `name` and `project` are required. `sortOrder` positions the milestone among its siblings. `project` is create-only — this tool does not move milestones between projects, so it is rejected on update. |
72
+ | `delete_project_milestone` | Delete a project milestone by ID. Issues assigned to it are detached, not deleted. |
71
73
 
72
74
  ### Initiatives
73
75
 
@@ -140,8 +140,9 @@ module LinearToonMcp
140
140
  # @param value [String]
141
141
  # @param scope [Hash] parent-scope kwargs (e.g. +team_id:+)
142
142
  # @return [String] resolved UUID
143
- # @raise [Error] when no attribute resolves the value
143
+ # @raise [Error] when +value+ is blank or no attribute resolves it
144
144
  def call(value:, **scope)
145
+ raise Error, "#{entity_label} must not be blank" if value.strip.empty?
145
146
  new(**scope).resolve(value)
146
147
  end
147
148
 
@@ -5,8 +5,30 @@ module LinearToonMcp
5
5
  # Resolves a Linear project status by name (workspace-scoped).
6
6
  class ProjectStatus < Base
7
7
  connection :projectStatuses
8
- filter_type :ProjectStatusFilter
9
- lookup_by :name
8
+
9
+ class << self
10
+ # Linear's projectStatuses field accepts no filter argument.
11
+ def query
12
+ @query ||= "query { #{connection_name} { nodes { id name } } }"
13
+ end
14
+ end
15
+
16
+ # Resolves +value+ to a UUID. UUIDs pass through unchanged; otherwise the
17
+ # workspace status list is matched by name, ignoring case.
18
+ #
19
+ # @raise [Error] when no status matches +value+
20
+ def resolve(value)
21
+ return value if value.match?(UUID_RE)
22
+
23
+ status = statuses.find { |node| node["name"].casecmp?(value) }
24
+ status&.fetch("id") || raise(Error, not_found_message(value))
25
+ end
26
+
27
+ private
28
+
29
+ def statuses
30
+ client.query(self.class.query).dig(self.class.connection_name, "nodes") || []
31
+ end
10
32
  end
11
33
  end
12
34
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinearToonMcp
4
+ module Tools
5
+ # Delete a project milestone by id. Issues assigned to the milestone
6
+ # are detached, not deleted.
7
+ class DeleteProjectMilestone < Delete
8
+ description "Delete a project milestone (issues are detached, not deleted)"
9
+
10
+ label "Milestone"
11
+
12
+ annotations(
13
+ read_only_hint: false,
14
+ destructive_hint: true,
15
+ idempotent_hint: false
16
+ )
17
+
18
+ input_schema(
19
+ properties: {
20
+ id: {type: "string", description: "Milestone ID"}
21
+ },
22
+ required: ["id"],
23
+ additionalProperties: false
24
+ )
25
+
26
+ MUTATION = <<~GRAPHQL
27
+ mutation($id: String!) {
28
+ projectMilestoneDelete(id: $id) { success entityId }
29
+ }
30
+ GRAPHQL
31
+
32
+ def variables(id:)
33
+ {id: id}
34
+ end
35
+ end
36
+ end
37
+ end
@@ -27,7 +27,7 @@ module LinearToonMcp
27
27
  priority: {type: "number", description: "0=None, 1=Urgent, 2=High, 3=Normal, 4=Low"},
28
28
  state: {type: "string", description: "State name or ID"},
29
29
  labels: {type: "array", items: {type: "string"}, description: "Label names or IDs"},
30
- project: {type: "string", description: "Project name or ID"},
30
+ project: {type: ["string", "null"], description: "Project name or ID. Null to remove (update only)"},
31
31
  cycle: {type: "string", description: "Cycle name, number, or ID"},
32
32
  estimate: {type: "number", description: "Issue estimate value"},
33
33
  dueDate: {type: "string", description: "Due date (ISO format)"},
@@ -200,8 +200,8 @@ module LinearToonMcp
200
200
  input[:labelIds] = Resolvers::IssueLabel.call_many(values: kwargs[:labels], team_id: team_id)
201
201
  end
202
202
  project_id = nil
203
- if kwargs.key?(:project) && kwargs[:project]
204
- project_id = Resolvers::Project.call(value: kwargs[:project])
203
+ if kwargs.key?(:project)
204
+ project_id = Resolvers::Project.call(value: kwargs[:project]) if kwargs[:project]
205
205
  input[:projectId] = project_id
206
206
  end
207
207
  if kwargs.key?(:milestone)
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinearToonMcp
4
+ module Tools
5
+ # Create or update a project milestone. +id+ presence determines
6
+ # create vs update. On create, +name+ and +project+ are required;
7
+ # +project+ is resolved via {Resolvers::Project}. +project+ is
8
+ # create-only — this tool does not move milestones between projects.
9
+ class SaveProjectMilestone < Base
10
+ description "Create or update a project milestone (id presence determines)"
11
+
12
+ annotations(
13
+ read_only_hint: false,
14
+ destructive_hint: false,
15
+ idempotent_hint: false
16
+ )
17
+
18
+ # standard:disable Layout/LineLength
19
+ input_schema(
20
+ properties: {
21
+ id: {type: "string", description: "Milestone ID. If provided, updates the existing milestone; otherwise creates"},
22
+ project: {type: "string", description: "Project name, ID, or slug (required when creating — rejected on update)"},
23
+ name: {type: "string", description: "Milestone name (required when creating)"},
24
+ description: {type: "string", description: "Description as Markdown"},
25
+ targetDate: {type: "string", description: "Target date (ISO format, YYYY-MM-DD)"},
26
+ sortOrder: {type: "number", description: "Position among the project's milestones — lower sorts first"}
27
+ },
28
+ additionalProperties: false
29
+ )
30
+ # standard:enable Layout/LineLength
31
+
32
+ RETURN_FIELDS = <<~GRAPHQL
33
+ id
34
+ name
35
+ description
36
+ targetDate
37
+ sortOrder
38
+ status
39
+ project { id name }
40
+ GRAPHQL
41
+
42
+ CREATE_MUTATION = <<~GRAPHQL
43
+ mutation($input: ProjectMilestoneCreateInput!) {
44
+ projectMilestoneCreate(input: $input) {
45
+ success
46
+ projectMilestone { #{RETURN_FIELDS.strip} }
47
+ }
48
+ }
49
+ GRAPHQL
50
+
51
+ UPDATE_MUTATION = <<~GRAPHQL
52
+ mutation($id: String!, $input: ProjectMilestoneUpdateInput!) {
53
+ projectMilestoneUpdate(id: $id, input: $input) {
54
+ success
55
+ projectMilestone { #{RETURN_FIELDS.strip} }
56
+ }
57
+ }
58
+ GRAPHQL
59
+
60
+ def perform(id: nil, **kwargs)
61
+ id ? update(id, kwargs) : create(kwargs)
62
+ end
63
+
64
+ private
65
+
66
+ def create(kwargs)
67
+ raise Error, "name is required when creating a milestone" unless kwargs[:name]
68
+ project = kwargs[:project] or raise Error, "project is required when creating a milestone"
69
+
70
+ input = build_input(kwargs).merge(projectId: Resolvers::Project.call(value: project))
71
+ submit(CREATE_MUTATION, "projectMilestoneCreate", input:)
72
+ end
73
+
74
+ def update(id, kwargs)
75
+ if kwargs.key?(:project)
76
+ raise Error, "Cannot pass `project` on update — this tool does not move milestones between projects"
77
+ end
78
+
79
+ submit(UPDATE_MUTATION, "projectMilestoneUpdate", id:, input: build_input(kwargs))
80
+ end
81
+
82
+ def submit(mutation, mutation_key, **variables)
83
+ data = client.query(mutation, variables:)
84
+ result = data[mutation_key] or raise Error, "Milestone save failed: no result returned"
85
+ raise Error, "Milestone save failed" unless result["success"]
86
+ result["projectMilestone"]
87
+ end
88
+
89
+ def build_input(kwargs)
90
+ {name: :name, description: :description, targetDate: :targetDate,
91
+ sortOrder: :sortOrder}.each_with_object({}) do |(key, field), input|
92
+ input[field] = kwargs[key] if kwargs.key?(key)
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -39,6 +39,8 @@ require_relative "tools/save_status_update"
39
39
  require_relative "tools/delete_status_update"
40
40
  require_relative "tools/save_project"
41
41
  require_relative "tools/archive_project"
42
+ require_relative "tools/save_project_milestone"
43
+ require_relative "tools/delete_project_milestone"
42
44
  require_relative "tools/get_team"
43
45
  require_relative "tools/get_user"
44
46
  require_relative "tools/get_issue_status"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LinearToonMcp
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.1"
5
5
  end
@@ -42,6 +42,7 @@ module LinearToonMcp
42
42
  Tools::ListStatusUpdates, Tools::GetStatusUpdate,
43
43
  Tools::SaveStatusUpdate, Tools::DeleteStatusUpdate,
44
44
  Tools::SaveProject, Tools::ArchiveProject,
45
+ Tools::SaveProjectMilestone, Tools::DeleteProjectMilestone,
45
46
  Tools::GetTeam, Tools::GetUser, Tools::GetIssueStatus,
46
47
  Tools::CreateIssueLabel
47
48
  ]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: linear-toon-mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yevhenii Hurin
@@ -85,6 +85,7 @@ files:
85
85
  - lib/linear_toon_mcp/tools/delete.rb
86
86
  - lib/linear_toon_mcp/tools/delete_comment.rb
87
87
  - lib/linear_toon_mcp/tools/delete_initiative.rb
88
+ - lib/linear_toon_mcp/tools/delete_project_milestone.rb
88
89
  - lib/linear_toon_mcp/tools/delete_status_update.rb
89
90
  - lib/linear_toon_mcp/tools/get.rb
90
91
  - lib/linear_toon_mcp/tools/get_initiative.rb
@@ -110,6 +111,7 @@ files:
110
111
  - lib/linear_toon_mcp/tools/save_initiative.rb
111
112
  - lib/linear_toon_mcp/tools/save_issue.rb
112
113
  - lib/linear_toon_mcp/tools/save_project.rb
114
+ - lib/linear_toon_mcp/tools/save_project_milestone.rb
113
115
  - lib/linear_toon_mcp/tools/save_status_update.rb
114
116
  - lib/linear_toon_mcp/version.rb
115
117
  homepage: https://github.com/hoblin/linear-toon-mcp