linear-mcp-fast 0.2.6__tar.gz → 0.3.0__tar.gz

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.
Files changed (44) hide show
  1. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/PKG-INFO +1 -1
  2. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/pyproject.toml +1 -1
  3. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/src/linear_mcp_fast/reader.py +95 -1
  4. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/src/linear_mcp_fast/store_detector.py +9 -0
  5. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/src/linear_mcp_fast.egg-info/PKG-INFO +1 -1
  6. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/README.md +0 -0
  7. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/setup.cfg +0 -0
  8. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/src/linear_mcp_fast/__init__.py +0 -0
  9. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/src/linear_mcp_fast/__main__.py +0 -0
  10. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/src/linear_mcp_fast/server.py +0 -0
  11. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/src/linear_mcp_fast.egg-info/SOURCES.txt +0 -0
  12. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/src/linear_mcp_fast.egg-info/dependency_links.txt +0 -0
  13. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/src/linear_mcp_fast.egg-info/entry_points.txt +0 -0
  14. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/src/linear_mcp_fast.egg-info/requires.txt +0 -0
  15. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/src/linear_mcp_fast.egg-info/top_level.txt +0 -0
  16. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/ccl_chromium_reader/__init__.py +0 -0
  17. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/ccl_chromium_reader/ccl_chromium_cache.py +0 -0
  18. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/ccl_chromium_reader/ccl_chromium_filesystem.py +0 -0
  19. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/ccl_chromium_reader/ccl_chromium_history.py +0 -0
  20. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/ccl_chromium_reader/ccl_chromium_indexeddb.py +0 -0
  21. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/ccl_chromium_reader/ccl_chromium_localstorage.py +0 -0
  22. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/ccl_chromium_reader/ccl_chromium_notifications.py +0 -0
  23. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/ccl_chromium_reader/ccl_chromium_profile_folder.py +0 -0
  24. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/ccl_chromium_reader/ccl_chromium_sessionstorage.py +0 -0
  25. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/ccl_chromium_reader/ccl_chromium_snss2.py +0 -0
  26. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/ccl_chromium_reader/ccl_shared_proto_db_downloads.py +0 -0
  27. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/ccl_chromium_reader/common.py +0 -0
  28. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/ccl_chromium_reader/download_common.py +0 -0
  29. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/ccl_chromium_reader/profile_folder_protocols.py +0 -0
  30. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/ccl_chromium_reader/serialization_formats/__init__.py +0 -0
  31. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/ccl_chromium_reader/serialization_formats/ccl_blink_value_deserializer.py +0 -0
  32. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/ccl_chromium_reader/serialization_formats/ccl_easy_chromium_pickle.py +0 -0
  33. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/ccl_chromium_reader/serialization_formats/ccl_protobuff.py +0 -0
  34. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/ccl_chromium_reader/serialization_formats/ccl_v8_value_deserializer.py +0 -0
  35. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/ccl_chromium_reader/storage_formats/__init__.py +0 -0
  36. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/ccl_chromium_reader/storage_formats/ccl_leveldb.py +0 -0
  37. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/tools_and_utilities/Chromium_dump_local_storage.py +0 -0
  38. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/tools_and_utilities/Chromium_dump_session_storage.py +0 -0
  39. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/tools_and_utilities/benchmark.py +0 -0
  40. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/tools_and_utilities/ccl_chrome_audit.py +0 -0
  41. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/tools_and_utilities/dump_indexeddb_details.py +0 -0
  42. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_chromium_reader/tools_and_utilities/dump_leveldb.py +0 -0
  43. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_simplesnappy/ccl_simplesnappy/__init__.py +0 -0
  44. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.0}/vendor/ccl_simplesnappy/ccl_simplesnappy/ccl_simplesnappy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linear-mcp-fast
3
- Version: 0.2.6
3
+ Version: 0.3.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "linear-mcp-fast"
7
- version = "0.2.6"
7
+ version = "0.3.0"
8
8
  description = "Fast MCP server for Linear - reads from Linear.app's local cache on macOS"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -5,8 +5,10 @@ Reads Linear's local IndexedDB cache to provide fast access to issues, users,
5
5
  teams, workflow states, and comments without API calls.
6
6
  """
7
7
 
8
+ import base64
8
9
  import json
9
10
  import os
11
+ import re
10
12
  import time
11
13
  from dataclasses import dataclass, field
12
14
  from typing import Any
@@ -36,6 +38,7 @@ class CachedData:
36
38
  comments: dict[str, dict[str, Any]] = field(default_factory=dict)
37
39
  comments_by_issue: dict[str, list[str]] = field(default_factory=dict)
38
40
  projects: dict[str, dict[str, Any]] = field(default_factory=dict)
41
+ issue_content: dict[str, str] = field(default_factory=dict) # issueId -> description
39
42
  loaded_at: float = 0.0
40
43
 
41
44
  def is_expired(self) -> bool:
@@ -92,6 +95,77 @@ class LinearLocalReader:
92
95
  return val.decode("utf-8", errors="replace")
93
96
  return str(val)
94
97
 
98
+ def _extract_yjs_text(self, content_state: str | None) -> str:
99
+ """Extract readable text from Y.js encoded contentState."""
100
+ if not content_state:
101
+ return ""
102
+
103
+ try:
104
+ decoded = base64.b64decode(content_state)
105
+ text = decoded.decode("utf-8", errors="replace")
106
+
107
+ # Extract readable text (Korean + ASCII printable)
108
+ readable = re.findall(r"[\uac00-\ud7af\u0020-\u007e]+", text)
109
+
110
+ # Structural markers to skip (ProseMirror/Y.js)
111
+ skip_exact = {
112
+ "prosemirror", "paragraph", "heading", "bullet_list", "list_item",
113
+ "ordered_list", "level", "link", "null", "strong", "em", "code",
114
+ "table", "table_row", "table_cell", "table_header", "colspan",
115
+ "rowspan", "colwidth", "issuemention", "label", "href", "title",
116
+ "order", "attrs", "content", "marks", "type", "text", "doc",
117
+ "blockquote", "code_block", "hard_break", "horizontal_rule",
118
+ "image", "suggestion_usermentions", "todo_item", "done", "language",
119
+ }
120
+
121
+ result = []
122
+ for r in readable:
123
+ r = r.strip()
124
+ if len(r) < 2:
125
+ continue
126
+
127
+ # Skip exact structural markers
128
+ if r.lower() in skip_exact:
129
+ continue
130
+
131
+ # Skip Y.js IDs and encoded strings
132
+ if re.match(r"^w[\$\)\(A-Z]", r):
133
+ continue
134
+
135
+ # Skip JSON objects and JSON-like patterns
136
+ if r.startswith("{") or '{"' in r:
137
+ continue
138
+
139
+ # Skip link markers with JSON
140
+ if r.startswith("link") and "{" in r:
141
+ continue
142
+
143
+ # Skip UUIDs
144
+ if re.match(r"^[a-f0-9-]{36}$", r):
145
+ continue
146
+
147
+ # Skip single characters or pure numbers
148
+ if len(r) <= 2 and not re.search(r"[\uac00-\ud7af]", r):
149
+ continue
150
+
151
+ # Skip strings that are mostly special characters
152
+ if len(r) > 0:
153
+ special_ratio = sum(1 for c in r if c in "()[]{}$#@*&^%") / len(r)
154
+ if special_ratio > 0.3:
155
+ continue
156
+
157
+ result.append(r)
158
+
159
+ # Join and clean up
160
+ text = " ".join(result)
161
+ # Remove excessive whitespace and parentheses artifacts
162
+ text = re.sub(r"\s*\(\s*$", "", text)
163
+ text = re.sub(r"^\s*\)\s*", "", text)
164
+ text = re.sub(r"\s+", " ", text)
165
+ return text.strip()
166
+ except Exception:
167
+ return ""
168
+
95
169
  def _extract_comment_text(self, body_data: Any) -> str:
96
170
  """Extract plain text from ProseMirror bodyData format."""
97
171
  if body_data is None:
@@ -185,11 +259,16 @@ class LinearLocalReader:
185
259
  team_key = team.get("key", "???")
186
260
  identifier = f"{team_key}-{val.get('number')}"
187
261
 
262
+ # Try descriptionData (ProseMirror format) first, fall back to description
263
+ description = val.get("description")
264
+ if not description and val.get("descriptionData"):
265
+ description = self._extract_comment_text(val.get("descriptionData"))
266
+
188
267
  cache.issues[val["id"]] = {
189
268
  "id": val["id"],
190
269
  "identifier": identifier,
191
270
  "title": val.get("title"),
192
- "description": val.get("description"),
271
+ "description": description,
193
272
  "number": val.get("number"),
194
273
  "priority": val.get("priority"),
195
274
  "estimate": val.get("estimate"),
@@ -246,6 +325,21 @@ class LinearLocalReader:
246
325
  "updatedAt": val.get("updatedAt"),
247
326
  }
248
327
 
328
+ # Load issue content (Y.js encoded descriptions)
329
+ if self._stores.issue_content:
330
+ for val in self._load_from_store(db, self._stores.issue_content):
331
+ issue_id = val.get("issueId")
332
+ content_state = val.get("contentState")
333
+ if issue_id and content_state:
334
+ extracted = self._extract_yjs_text(content_state)
335
+ if extracted:
336
+ cache.issue_content[issue_id] = extracted
337
+
338
+ # Update issues with descriptions from issue_content
339
+ for issue_id, desc in cache.issue_content.items():
340
+ if issue_id in cache.issues and not cache.issues[issue_id].get("description"):
341
+ cache.issues[issue_id]["description"] = desc
342
+
249
343
  self._cache = cache
250
344
 
251
345
  def _ensure_cache(self) -> CachedData:
@@ -21,6 +21,7 @@ class DetectedStores:
21
21
  workflow_states: list[str] | None = None
22
22
  comments: str | None = None
23
23
  projects: str | None = None
24
+ issue_content: str | None = None # Y.js encoded issue descriptions
24
25
 
25
26
 
26
27
  def _is_issue_record(record: dict[str, Any]) -> bool:
@@ -68,6 +69,12 @@ def _is_project_record(record: dict[str, Any]) -> bool:
68
69
  return required.issubset(record.keys())
69
70
 
70
71
 
72
+ def _is_issue_content_record(record: dict[str, Any]) -> bool:
73
+ """Check if a record looks like issue content (Y.js encoded description)."""
74
+ required = {"issueId", "contentState"}
75
+ return required.issubset(record.keys())
76
+
77
+
71
78
  def detect_stores(db: ccl_chromium_indexeddb.WrappedDatabase) -> DetectedStores:
72
79
  """
73
80
  Detect object stores by sampling their first record.
@@ -109,6 +116,8 @@ def detect_stores(db: ccl_chromium_indexeddb.WrappedDatabase) -> DetectedStores:
109
116
  result.comments = store_name
110
117
  elif _is_project_record(val) and result.projects is None:
111
118
  result.projects = store_name
119
+ elif _is_issue_content_record(val) and result.issue_content is None:
120
+ result.issue_content = store_name
112
121
 
113
122
  break # Only check first record
114
123
  except Exception:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linear-mcp-fast
3
- Version: 0.2.6
3
+ Version: 0.3.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