linear-mcp-fast 0.1.0__py3-none-any.whl → 0.2.0__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,25 +31,6 @@ 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,
@@ -171,56 +152,13 @@ def get_issue(identifier: str) -> dict[str, Any] | None:
171
152
 
172
153
 
173
154
  @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(
155
+ def list_my_issues(
218
156
  name: str,
219
157
  state_type: str | None = None,
220
158
  limit: int = 30,
221
159
  ) -> dict[str, Any]:
222
160
  """
223
- Get issues assigned to a user.
161
+ List issues assigned to a user.
224
162
 
225
163
  Args:
226
164
  name: User name to search for
@@ -342,24 +280,182 @@ def list_projects(team: str | None = None) -> list[dict[str, Any]]:
342
280
 
343
281
 
344
282
  @mcp.tool()
345
- def get_summary() -> dict[str, Any]:
283
+ def get_team(team: str) -> dict[str, Any] | None:
284
+ """
285
+ Get team details by key or name.
286
+
287
+ Args:
288
+ team: Team key (e.g., 'UK') or name
289
+
290
+ Returns:
291
+ Team details or None if not found
292
+ """
293
+ reader = get_reader()
294
+ team_obj = reader.find_team(team)
295
+
296
+ if not team_obj:
297
+ return None
298
+
299
+ issue_count = sum(
300
+ 1 for i in reader.issues.values() if i.get("teamId") == team_obj["id"]
301
+ )
302
+
303
+ # Count by state type
304
+ state_counts: dict[str, int] = {}
305
+ for issue in reader.issues.values():
306
+ if issue.get("teamId") == team_obj["id"]:
307
+ state_type = reader.get_state_type(issue.get("stateId", ""))
308
+ state_counts[state_type] = state_counts.get(state_type, 0) + 1
309
+
310
+ return {
311
+ "id": team_obj.get("id"),
312
+ "key": team_obj.get("key"),
313
+ "name": team_obj.get("name"),
314
+ "description": team_obj.get("description"),
315
+ "issueCount": issue_count,
316
+ "issuesByState": state_counts,
317
+ }
318
+
319
+
320
+ @mcp.tool()
321
+ def get_project(name: str) -> dict[str, Any] | None:
322
+ """
323
+ Get project details by name.
324
+
325
+ Args:
326
+ name: Project name (partial match)
327
+
328
+ Returns:
329
+ Project details or None if not found
330
+ """
331
+ reader = get_reader()
332
+
333
+ # Find project by name (partial match)
334
+ name_lower = name.lower()
335
+ project = None
336
+ for p in reader.projects.values():
337
+ if name_lower in (p.get("name", "") or "").lower():
338
+ project = p
339
+ break
340
+
341
+ if not project:
342
+ return None
343
+
344
+ issue_count = sum(
345
+ 1 for i in reader.issues.values() if i.get("projectId") == project["id"]
346
+ )
347
+
348
+ # Count by state type
349
+ state_counts: dict[str, int] = {}
350
+ for issue in reader.issues.values():
351
+ if issue.get("projectId") == project["id"]:
352
+ state_type = reader.get_state_type(issue.get("stateId", ""))
353
+ state_counts[state_type] = state_counts.get(state_type, 0) + 1
354
+
355
+ return {
356
+ "id": project.get("id"),
357
+ "name": project.get("name"),
358
+ "description": project.get("description"),
359
+ "state": project.get("state"),
360
+ "startDate": project.get("startDate"),
361
+ "targetDate": project.get("targetDate"),
362
+ "issueCount": issue_count,
363
+ "issuesByState": state_counts,
364
+ }
365
+
366
+
367
+ @mcp.tool()
368
+ def list_users() -> list[dict[str, Any]]:
369
+ """
370
+ List all users in the workspace.
371
+
372
+ Returns:
373
+ List of users with basic info
374
+ """
375
+ reader = get_reader()
376
+ results = []
377
+
378
+ for user in reader.users.values():
379
+ issue_count = sum(
380
+ 1 for i in reader.issues.values() if i.get("assigneeId") == user["id"]
381
+ )
382
+ results.append({
383
+ "id": user.get("id"),
384
+ "name": user.get("name"),
385
+ "email": user.get("email"),
386
+ "displayName": user.get("displayName"),
387
+ "assignedIssueCount": issue_count,
388
+ })
389
+
390
+ results.sort(key=lambda x: x.get("name", "") or "")
391
+ return results
392
+
393
+
394
+ @mcp.tool()
395
+ def get_user(name: str) -> dict[str, Any] | None:
346
396
  """
347
- Get a summary of local cache data.
397
+ Get user details by name.
398
+
399
+ Args:
400
+ name: User name (partial match)
348
401
 
349
402
  Returns:
350
- Counts of teams, users, issues, projects, comments
403
+ User details or None if not found
351
404
  """
352
405
  reader = get_reader()
353
- summary = reader.get_summary()
406
+ user = reader.find_user(name)
354
407
 
355
- # Add state breakdown
408
+ if not user:
409
+ return None
410
+
411
+ # Count issues by state
356
412
  state_counts: dict[str, int] = {}
357
413
  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
414
+ if issue.get("assigneeId") == user["id"]:
415
+ state_type = reader.get_state_type(issue.get("stateId", ""))
416
+ state_counts[state_type] = state_counts.get(state_type, 0) + 1
417
+
418
+ return {
419
+ "id": user.get("id"),
420
+ "name": user.get("name"),
421
+ "email": user.get("email"),
422
+ "displayName": user.get("displayName"),
423
+ "assignedIssueCount": sum(state_counts.values()),
424
+ "issuesByState": state_counts,
425
+ }
426
+
427
+
428
+ @mcp.tool()
429
+ def list_issue_statuses(team: str) -> list[dict[str, Any]]:
430
+ """
431
+ List available issue statuses for a team.
432
+
433
+ Args:
434
+ team: Team key (e.g., 'UK')
360
435
 
361
- summary["issuesByState"] = state_counts
362
- return summary
436
+ Returns:
437
+ List of workflow states
438
+ """
439
+ reader = get_reader()
440
+
441
+ team_obj = reader.find_team(team)
442
+ if not team_obj:
443
+ return []
444
+
445
+ # Get states for this team
446
+ results = []
447
+ for state in reader.states.values():
448
+ if state.get("teamId") == team_obj["id"]:
449
+ results.append({
450
+ "id": state.get("id"),
451
+ "name": state.get("name"),
452
+ "type": state.get("type"),
453
+ "color": state.get("color"),
454
+ "position": state.get("position"),
455
+ })
456
+
457
+ results.sort(key=lambda x: (x.get("position") or 0))
458
+ return results
363
459
 
364
460
 
365
461
  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.0
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=cmaU5sAw1RPnRkrT78HitNIzoKj4UA7TP1ndI5GYgWA,12761
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.0.dist-info/METADATA,sha256=zlyAyW9pYdrIPV3ndxoWSJjdQPhpDxaG4lTSI9hDH-8,3812
36
+ linear_mcp_fast-0.2.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
37
+ linear_mcp_fast-0.2.0.dist-info/entry_points.txt,sha256=Aa98tAkWz_08mS_SRyfyx0k3PuMBQoMygT88HCKMyWk,57
38
+ linear_mcp_fast-0.2.0.dist-info/top_level.txt,sha256=j-O2BoBpFBpGyTl2V1cp0ZjxZAQwpkweeNxG4BcQ7io,73
39
+ linear_mcp_fast-0.2.0.dist-info/RECORD,,