linear-mcp-fast 0.1.0__py3-none-any.whl → 0.2.1__py3-none-any.whl

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.
linear_mcp_fast/reader.py CHANGED
@@ -171,6 +171,8 @@ class LinearLocalReader:
171
171
  "name": val.get("name"),
172
172
  "type": val.get("type"),
173
173
  "color": val.get("color"),
174
+ "teamId": val.get("teamId"),
175
+ "position": val.get("position"),
174
176
  }
175
177
 
176
178
  # Load issues
linear_mcp_fast/server.py CHANGED
@@ -31,32 +31,13 @@ def get_reader() -> LinearLocalReader:
31
31
  return _reader
32
32
 
33
33
 
34
- def _parse_datetime(dt_value: Any) -> float | None:
35
- """Parse a datetime value to Unix timestamp."""
36
- if dt_value is None:
37
- return None
38
- if isinstance(dt_value, (int, float)):
39
- if dt_value > 1e12:
40
- return dt_value / 1000
41
- return dt_value
42
- if isinstance(dt_value, str):
43
- from datetime import datetime
44
- try:
45
- dt_str = dt_value.replace("Z", "+00:00")
46
- dt = datetime.fromisoformat(dt_str)
47
- return dt.timestamp()
48
- except ValueError:
49
- return None
50
- return None
51
-
52
-
53
34
  @mcp.tool()
54
35
  def list_issues(
55
36
  assignee: str | None = None,
56
37
  team: str | None = None,
57
38
  state_type: str | None = None,
58
39
  priority: int | None = None,
59
- limit: int = 50,
40
+ limit: int | None = None,
60
41
  ) -> dict[str, Any]:
61
42
  """
62
43
  List issues from local cache with optional filters.
@@ -66,13 +47,12 @@ def list_issues(
66
47
  team: Filter by team key (e.g., 'UK')
67
48
  state_type: Filter by state type (started, unstarted, completed, canceled, backlog)
68
49
  priority: Filter by priority (1=Urgent, 2=High, 3=Medium, 4=Low)
69
- limit: Maximum number of issues (default 50, max 100)
50
+ limit: Maximum number of issues (default: all)
70
51
 
71
52
  Returns:
72
53
  Dictionary with issues array and totalCount
73
54
  """
74
55
  reader = get_reader()
75
- limit = min(limit, 100)
76
56
 
77
57
  assignee_id = None
78
58
  if assignee:
@@ -108,7 +88,7 @@ def list_issues(
108
88
  filtered.append(issue)
109
89
 
110
90
  total_count = len(filtered)
111
- page = filtered[:limit]
91
+ page = filtered[:limit] if limit else filtered
112
92
 
113
93
  results = []
114
94
  for issue in page:
@@ -171,67 +151,23 @@ def get_issue(identifier: str) -> dict[str, Any] | None:
171
151
 
172
152
 
173
153
  @mcp.tool()
174
- def search_issues(query: str, limit: int = 20) -> dict[str, Any]:
175
- """
176
- Search issues by title.
177
-
178
- Args:
179
- query: Search query (case-insensitive)
180
- limit: Maximum results (default 20, max 100)
181
-
182
- Returns:
183
- Dictionary with matching issues
184
- """
185
- reader = get_reader()
186
- limit = min(limit, 100)
187
- query_lower = query.lower()
188
-
189
- all_issues = sorted(
190
- reader.issues.values(),
191
- key=lambda x: (x.get("priority") or 4, x.get("id", "")),
192
- )
193
-
194
- filtered = []
195
- for issue in all_issues:
196
- title = issue.get("title", "") or ""
197
- identifier = issue.get("identifier", "") or ""
198
- if query_lower in title.lower() or query_lower in identifier.lower():
199
- filtered.append(issue)
200
-
201
- match_count = len(filtered)
202
- page = filtered[:limit]
203
-
204
- results = []
205
- for issue in page:
206
- results.append({
207
- "identifier": issue.get("identifier"),
208
- "title": issue.get("title"),
209
- "state": reader.get_state_name(issue.get("stateId", "")),
210
- "stateType": reader.get_state_type(issue.get("stateId", "")),
211
- })
212
-
213
- return {"issues": results, "matchCount": match_count}
214
-
215
-
216
- @mcp.tool()
217
- def get_my_issues(
154
+ def list_my_issues(
218
155
  name: str,
219
156
  state_type: str | None = None,
220
- limit: int = 30,
157
+ limit: int | None = None,
221
158
  ) -> dict[str, Any]:
222
159
  """
223
- Get issues assigned to a user.
160
+ List issues assigned to a user.
224
161
 
225
162
  Args:
226
163
  name: User name to search for
227
164
  state_type: Optional filter (started, unstarted, completed, canceled, backlog)
228
- limit: Maximum issues (default 30, max 100)
165
+ limit: Maximum issues (default: all)
229
166
 
230
167
  Returns:
231
168
  User info with their issues
232
169
  """
233
170
  reader = get_reader()
234
- limit = min(limit, 100)
235
171
 
236
172
  user = reader.find_user(name)
237
173
  if not user:
@@ -253,7 +189,7 @@ def get_my_issues(
253
189
  if reader.get_state_type(i.get("stateId", "")) == state_type
254
190
  ]
255
191
 
256
- page = all_issues[:limit]
192
+ page = all_issues[:limit] if limit else all_issues
257
193
 
258
194
  results = []
259
195
  for issue in page:
@@ -342,24 +278,182 @@ def list_projects(team: str | None = None) -> list[dict[str, Any]]:
342
278
 
343
279
 
344
280
  @mcp.tool()
345
- def get_summary() -> dict[str, Any]:
281
+ def get_team(team: str) -> dict[str, Any] | None:
282
+ """
283
+ Get team details by key or name.
284
+
285
+ Args:
286
+ team: Team key (e.g., 'UK') or name
287
+
288
+ Returns:
289
+ Team details or None if not found
290
+ """
291
+ reader = get_reader()
292
+ team_obj = reader.find_team(team)
293
+
294
+ if not team_obj:
295
+ return None
296
+
297
+ issue_count = sum(
298
+ 1 for i in reader.issues.values() if i.get("teamId") == team_obj["id"]
299
+ )
300
+
301
+ # Count by state type
302
+ state_counts: dict[str, int] = {}
303
+ for issue in reader.issues.values():
304
+ if issue.get("teamId") == team_obj["id"]:
305
+ state_type = reader.get_state_type(issue.get("stateId", ""))
306
+ state_counts[state_type] = state_counts.get(state_type, 0) + 1
307
+
308
+ return {
309
+ "id": team_obj.get("id"),
310
+ "key": team_obj.get("key"),
311
+ "name": team_obj.get("name"),
312
+ "description": team_obj.get("description"),
313
+ "issueCount": issue_count,
314
+ "issuesByState": state_counts,
315
+ }
316
+
317
+
318
+ @mcp.tool()
319
+ def get_project(name: str) -> dict[str, Any] | None:
320
+ """
321
+ Get project details by name.
322
+
323
+ Args:
324
+ name: Project name (partial match)
325
+
326
+ Returns:
327
+ Project details or None if not found
328
+ """
329
+ reader = get_reader()
330
+
331
+ # Find project by name (partial match)
332
+ name_lower = name.lower()
333
+ project = None
334
+ for p in reader.projects.values():
335
+ if name_lower in (p.get("name", "") or "").lower():
336
+ project = p
337
+ break
338
+
339
+ if not project:
340
+ return None
341
+
342
+ issue_count = sum(
343
+ 1 for i in reader.issues.values() if i.get("projectId") == project["id"]
344
+ )
345
+
346
+ # Count by state type
347
+ state_counts: dict[str, int] = {}
348
+ for issue in reader.issues.values():
349
+ if issue.get("projectId") == project["id"]:
350
+ state_type = reader.get_state_type(issue.get("stateId", ""))
351
+ state_counts[state_type] = state_counts.get(state_type, 0) + 1
352
+
353
+ return {
354
+ "id": project.get("id"),
355
+ "name": project.get("name"),
356
+ "description": project.get("description"),
357
+ "state": project.get("state"),
358
+ "startDate": project.get("startDate"),
359
+ "targetDate": project.get("targetDate"),
360
+ "issueCount": issue_count,
361
+ "issuesByState": state_counts,
362
+ }
363
+
364
+
365
+ @mcp.tool()
366
+ def list_users() -> list[dict[str, Any]]:
367
+ """
368
+ List all users in the workspace.
369
+
370
+ Returns:
371
+ List of users with basic info
372
+ """
373
+ reader = get_reader()
374
+ results = []
375
+
376
+ for user in reader.users.values():
377
+ issue_count = sum(
378
+ 1 for i in reader.issues.values() if i.get("assigneeId") == user["id"]
379
+ )
380
+ results.append({
381
+ "id": user.get("id"),
382
+ "name": user.get("name"),
383
+ "email": user.get("email"),
384
+ "displayName": user.get("displayName"),
385
+ "assignedIssueCount": issue_count,
386
+ })
387
+
388
+ results.sort(key=lambda x: x.get("name", "") or "")
389
+ return results
390
+
391
+
392
+ @mcp.tool()
393
+ def get_user(name: str) -> dict[str, Any] | None:
346
394
  """
347
- Get a summary of local cache data.
395
+ Get user details by name.
396
+
397
+ Args:
398
+ name: User name (partial match)
348
399
 
349
400
  Returns:
350
- Counts of teams, users, issues, projects, comments
401
+ User details or None if not found
351
402
  """
352
403
  reader = get_reader()
353
- summary = reader.get_summary()
404
+ user = reader.find_user(name)
354
405
 
355
- # Add state breakdown
406
+ if not user:
407
+ return None
408
+
409
+ # Count issues by state
356
410
  state_counts: dict[str, int] = {}
357
411
  for issue in reader.issues.values():
358
- state_type = reader.get_state_type(issue.get("stateId", ""))
359
- state_counts[state_type] = state_counts.get(state_type, 0) + 1
412
+ if issue.get("assigneeId") == user["id"]:
413
+ state_type = reader.get_state_type(issue.get("stateId", ""))
414
+ state_counts[state_type] = state_counts.get(state_type, 0) + 1
415
+
416
+ return {
417
+ "id": user.get("id"),
418
+ "name": user.get("name"),
419
+ "email": user.get("email"),
420
+ "displayName": user.get("displayName"),
421
+ "assignedIssueCount": sum(state_counts.values()),
422
+ "issuesByState": state_counts,
423
+ }
424
+
425
+
426
+ @mcp.tool()
427
+ def list_issue_statuses(team: str) -> list[dict[str, Any]]:
428
+ """
429
+ List available issue statuses for a team.
430
+
431
+ Args:
432
+ team: Team key (e.g., 'UK')
360
433
 
361
- summary["issuesByState"] = state_counts
362
- return summary
434
+ Returns:
435
+ List of workflow states
436
+ """
437
+ reader = get_reader()
438
+
439
+ team_obj = reader.find_team(team)
440
+ if not team_obj:
441
+ return []
442
+
443
+ # Get states for this team
444
+ results = []
445
+ for state in reader.states.values():
446
+ if state.get("teamId") == team_obj["id"]:
447
+ results.append({
448
+ "id": state.get("id"),
449
+ "name": state.get("name"),
450
+ "type": state.get("type"),
451
+ "color": state.get("color"),
452
+ "position": state.get("position"),
453
+ })
454
+
455
+ results.sort(key=lambda x: (x.get("position") or 0))
456
+ return results
363
457
 
364
458
 
365
459
  def main():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linear-mcp-fast
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: Fast MCP server for Linear - reads from Linear.app's local cache on macOS
5
5
  Author: everything-chalna
6
6
  License-Expression: MIT
@@ -110,11 +110,14 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
110
110
  |------|-------------|
111
111
  | `list_issues` | List issues with filters (assignee, team, state, priority) |
112
112
  | `get_issue` | Get issue details with comments |
113
- | `search_issues` | Search issues by title |
114
- | `get_my_issues` | Get issues assigned to a user |
113
+ | `list_my_issues` | List issues assigned to a user |
115
114
  | `list_teams` | List all teams |
115
+ | `get_team` | Get team details |
116
116
  | `list_projects` | List all projects |
117
- | `get_summary` | Get cache summary |
117
+ | `get_project` | Get project details |
118
+ | `list_users` | List all users |
119
+ | `get_user` | Get user details |
120
+ | `list_issue_statuses` | List workflow states for a team |
118
121
 
119
122
  ### Writing (official Linear MCP)
120
123
 
@@ -23,8 +23,8 @@ ccl_simplesnappy/__init__.py,sha256=OqArK0MfdVl2oMw1MwpWSYWWVLFT-VLIWnEXXCsacJo,
23
23
  ccl_simplesnappy/ccl_simplesnappy.py,sha256=dLv1wejr2vCa2b_ZinozXVfcSsZIzJrt5ZkyxA3cQXA,10461
24
24
  linear_mcp_fast/__init__.py,sha256=T-ioPzoZXC3a_zLilZuDBCXUn8nDjvITS7svk7BwWmY,138
25
25
  linear_mcp_fast/__main__.py,sha256=2wkhXADcE2oGdtEpGrIvvEe9YGKjpwnJ3DBWghkVQKk,124
26
- linear_mcp_fast/reader.py,sha256=lf-bHccLSzmP0Ez4UeRLLYoYvVwlZOByWnfcUTtuuV0,16249
27
- linear_mcp_fast/server.py,sha256=cz-kSNy4y4xSRJRx_x9IyNl7TlwU9U3rA0pH1ro3QWs,10389
26
+ linear_mcp_fast/reader.py,sha256=SefUC45D26BrvbOqHI4eGBzQWRHgRYY1R-RpMdwPB24,16367
27
+ linear_mcp_fast/server.py,sha256=Ha9kCLb_-V7c0HTSz3navb9EEWNmsTh320vRhI8ppmw,12757
28
28
  linear_mcp_fast/store_detector.py,sha256=5cnTwS-LPsgS9NivNxKa3bPv4mwAnZsi40-kXszejMI,4142
29
29
  tools_and_utilities/Chromium_dump_local_storage.py,sha256=gG-pKFFk6lo332LQy2JvInlQh9Zldm5zAsuibb-dBkQ,4337
30
30
  tools_and_utilities/Chromium_dump_session_storage.py,sha256=17BKFWioo6fPwYkH58QycJCA-z85RtWMBXsJ_29hHQs,3484
@@ -32,8 +32,8 @@ tools_and_utilities/benchmark.py,sha256=fyD5U6yI7Y0TkyhYtvvaHyk9Y2jJe2yxYWoFPQWy
32
32
  tools_and_utilities/ccl_chrome_audit.py,sha256=irGyYJae0apZDZCn23jMKmY3tYQgWyZEL8vdUBcHLZk,24695
33
33
  tools_and_utilities/dump_indexeddb_details.py,sha256=ipNWLKPQoSNhCtPHKWvMWpKu8FhCnvc4Rciyx-90boI,2298
34
34
  tools_and_utilities/dump_leveldb.py,sha256=hj7QnOHG64KK2fKsZ9qQOVqUUmHUtxUZqPYl4EZJO9U,1882
35
- linear_mcp_fast-0.1.0.dist-info/METADATA,sha256=0aRAuDTEJ6TQDu1tEkJQAtCkXbEftJ1CLCukOGdg8p8,3691
36
- linear_mcp_fast-0.1.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
37
- linear_mcp_fast-0.1.0.dist-info/entry_points.txt,sha256=Aa98tAkWz_08mS_SRyfyx0k3PuMBQoMygT88HCKMyWk,57
38
- linear_mcp_fast-0.1.0.dist-info/top_level.txt,sha256=j-O2BoBpFBpGyTl2V1cp0ZjxZAQwpkweeNxG4BcQ7io,73
39
- linear_mcp_fast-0.1.0.dist-info/RECORD,,
35
+ linear_mcp_fast-0.2.1.dist-info/METADATA,sha256=Drqt5Jk8GBcPEvb_cvk96d4YsdhDR47rl05rI4Q5B-c,3812
36
+ linear_mcp_fast-0.2.1.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
37
+ linear_mcp_fast-0.2.1.dist-info/entry_points.txt,sha256=Aa98tAkWz_08mS_SRyfyx0k3PuMBQoMygT88HCKMyWk,57
38
+ linear_mcp_fast-0.2.1.dist-info/top_level.txt,sha256=j-O2BoBpFBpGyTl2V1cp0ZjxZAQwpkweeNxG4BcQ7io,73
39
+ linear_mcp_fast-0.2.1.dist-info/RECORD,,