linear-mcp-fast 0.2.6__tar.gz → 0.3.1__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.1}/PKG-INFO +12 -2
  2. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.1}/README.md +11 -1
  3. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.1}/pyproject.toml +1 -1
  4. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.1}/src/linear_mcp_fast/reader.py +95 -1
  5. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.1}/src/linear_mcp_fast/store_detector.py +9 -0
  6. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.1}/src/linear_mcp_fast.egg-info/PKG-INFO +12 -2
  7. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.1}/setup.cfg +0 -0
  8. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.1}/src/linear_mcp_fast/__init__.py +0 -0
  9. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.1}/src/linear_mcp_fast/__main__.py +0 -0
  10. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.1}/src/linear_mcp_fast/server.py +0 -0
  11. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.1}/src/linear_mcp_fast.egg-info/SOURCES.txt +0 -0
  12. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.1}/src/linear_mcp_fast.egg-info/dependency_links.txt +0 -0
  13. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.1}/src/linear_mcp_fast.egg-info/entry_points.txt +0 -0
  14. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.1}/src/linear_mcp_fast.egg-info/requires.txt +0 -0
  15. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.1}/src/linear_mcp_fast.egg-info/top_level.txt +0 -0
  16. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.1}/vendor/ccl_chromium_reader/ccl_chromium_reader/__init__.py +0 -0
  17. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.1}/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.1}/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.1}/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.1}/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.1}/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.1}/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.1}/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.1}/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.1}/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.1}/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.1}/vendor/ccl_chromium_reader/ccl_chromium_reader/common.py +0 -0
  28. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.1}/vendor/ccl_chromium_reader/ccl_chromium_reader/download_common.py +0 -0
  29. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.1}/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.1}/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.1}/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.1}/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.1}/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.1}/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.1}/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.1}/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.1}/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.1}/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.1}/vendor/ccl_chromium_reader/tools_and_utilities/benchmark.py +0 -0
  40. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.1}/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.1}/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.1}/vendor/ccl_chromium_reader/tools_and_utilities/dump_leveldb.py +0 -0
  43. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.1}/vendor/ccl_simplesnappy/ccl_simplesnappy/__init__.py +0 -0
  44. {linear_mcp_fast-0.2.6 → linear_mcp_fast-0.3.1}/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.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
@@ -32,18 +32,22 @@ While using the official Linear MCP with Claude Code, I noticed that **read oper
32
32
 
33
33
  The problem:
34
34
  - Official Linear MCP makes API calls for every read
35
+ - Issue descriptions require separate API calls
35
36
  - Responses include excessive metadata (full user objects, workflow states, etc.)
36
37
  - Context window fills up quickly when exploring issues
37
38
  - Slower response times due to network latency
38
39
 
39
40
  My solution: **Read directly from Linear.app's local cache.**
40
41
 
42
+ Linear.app stores issue descriptions in Y.js CRDT format. This package decodes them locally, so you get descriptions without API calls.
43
+
41
44
  Linear.app (Electron) syncs all your data to a local IndexedDB. This MCP server reads from that cache, giving you:
42
45
 
43
46
  - **Zero API calls** - Instant reads from disk
44
47
  - **Smaller responses** - Only the fields you need
45
48
  - **Offline access** - Works without internet
46
49
  - **Faster iteration** - No rate limits, no latency
50
+ - **Issue descriptions** - Extracts text from Y.js encoded content (v0.3.0+)
47
51
 
48
52
  ## Requirements
49
53
 
@@ -146,10 +150,16 @@ IndexedDB (LevelDB)
146
150
  ~/Library/Application Support/Linear/IndexedDB/...
147
151
  ↓ read by
148
152
  linear-mcp-fast
149
-
153
+ decodes Y.js CRDT content
150
154
  Fast, offline access to issues, teams, users, projects
151
155
  ```
152
156
 
157
+ ### Issue Descriptions
158
+
159
+ Linear stores issue descriptions in a separate `contentState` field using Y.js CRDT encoding. This package decodes the binary format to extract readable text, so `get_issue` returns the description without an API call.
160
+
161
+ Note: The extraction is text-based (not full Y.js parsing), so some formatting may be lost. For rich markdown content, use the official Linear MCP.
162
+
153
163
  ## Troubleshooting
154
164
 
155
165
  **"Linear database not found"**
@@ -8,18 +8,22 @@ While using the official Linear MCP with Claude Code, I noticed that **read oper
8
8
 
9
9
  The problem:
10
10
  - Official Linear MCP makes API calls for every read
11
+ - Issue descriptions require separate API calls
11
12
  - Responses include excessive metadata (full user objects, workflow states, etc.)
12
13
  - Context window fills up quickly when exploring issues
13
14
  - Slower response times due to network latency
14
15
 
15
16
  My solution: **Read directly from Linear.app's local cache.**
16
17
 
18
+ Linear.app stores issue descriptions in Y.js CRDT format. This package decodes them locally, so you get descriptions without API calls.
19
+
17
20
  Linear.app (Electron) syncs all your data to a local IndexedDB. This MCP server reads from that cache, giving you:
18
21
 
19
22
  - **Zero API calls** - Instant reads from disk
20
23
  - **Smaller responses** - Only the fields you need
21
24
  - **Offline access** - Works without internet
22
25
  - **Faster iteration** - No rate limits, no latency
26
+ - **Issue descriptions** - Extracts text from Y.js encoded content (v0.3.0+)
23
27
 
24
28
  ## Requirements
25
29
 
@@ -122,10 +126,16 @@ IndexedDB (LevelDB)
122
126
  ~/Library/Application Support/Linear/IndexedDB/...
123
127
  ↓ read by
124
128
  linear-mcp-fast
125
-
129
+ decodes Y.js CRDT content
126
130
  Fast, offline access to issues, teams, users, projects
127
131
  ```
128
132
 
133
+ ### Issue Descriptions
134
+
135
+ Linear stores issue descriptions in a separate `contentState` field using Y.js CRDT encoding. This package decodes the binary format to extract readable text, so `get_issue` returns the description without an API call.
136
+
137
+ Note: The extraction is text-based (not full Y.js parsing), so some formatting may be lost. For rich markdown content, use the official Linear MCP.
138
+
129
139
  ## Troubleshooting
130
140
 
131
141
  **"Linear database not found"**
@@ -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.1"
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.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
@@ -32,18 +32,22 @@ While using the official Linear MCP with Claude Code, I noticed that **read oper
32
32
 
33
33
  The problem:
34
34
  - Official Linear MCP makes API calls for every read
35
+ - Issue descriptions require separate API calls
35
36
  - Responses include excessive metadata (full user objects, workflow states, etc.)
36
37
  - Context window fills up quickly when exploring issues
37
38
  - Slower response times due to network latency
38
39
 
39
40
  My solution: **Read directly from Linear.app's local cache.**
40
41
 
42
+ Linear.app stores issue descriptions in Y.js CRDT format. This package decodes them locally, so you get descriptions without API calls.
43
+
41
44
  Linear.app (Electron) syncs all your data to a local IndexedDB. This MCP server reads from that cache, giving you:
42
45
 
43
46
  - **Zero API calls** - Instant reads from disk
44
47
  - **Smaller responses** - Only the fields you need
45
48
  - **Offline access** - Works without internet
46
49
  - **Faster iteration** - No rate limits, no latency
50
+ - **Issue descriptions** - Extracts text from Y.js encoded content (v0.3.0+)
47
51
 
48
52
  ## Requirements
49
53
 
@@ -146,10 +150,16 @@ IndexedDB (LevelDB)
146
150
  ~/Library/Application Support/Linear/IndexedDB/...
147
151
  ↓ read by
148
152
  linear-mcp-fast
149
-
153
+ decodes Y.js CRDT content
150
154
  Fast, offline access to issues, teams, users, projects
151
155
  ```
152
156
 
157
+ ### Issue Descriptions
158
+
159
+ Linear stores issue descriptions in a separate `contentState` field using Y.js CRDT encoding. This package decodes the binary format to extract readable text, so `get_issue` returns the description without an API call.
160
+
161
+ Note: The extraction is text-based (not full Y.js parsing), so some formatting may be lost. For rich markdown content, use the official Linear MCP.
162
+
153
163
  ## Troubleshooting
154
164
 
155
165
  **"Linear database not found"**