mcp-ticketer 0.4.11__py3-none-any.whl → 2.0.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.

Potentially problematic release.


This version of mcp-ticketer might be problematic. Click here for more details.

Files changed (111) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/adapters/__init__.py +2 -0
  4. mcp_ticketer/adapters/aitrackdown.py +394 -9
  5. mcp_ticketer/adapters/asana/__init__.py +15 -0
  6. mcp_ticketer/adapters/asana/adapter.py +1416 -0
  7. mcp_ticketer/adapters/asana/client.py +292 -0
  8. mcp_ticketer/adapters/asana/mappers.py +348 -0
  9. mcp_ticketer/adapters/asana/types.py +146 -0
  10. mcp_ticketer/adapters/github.py +836 -105
  11. mcp_ticketer/adapters/hybrid.py +47 -5
  12. mcp_ticketer/adapters/jira.py +772 -1
  13. mcp_ticketer/adapters/linear/adapter.py +2293 -108
  14. mcp_ticketer/adapters/linear/client.py +146 -12
  15. mcp_ticketer/adapters/linear/mappers.py +105 -11
  16. mcp_ticketer/adapters/linear/queries.py +168 -1
  17. mcp_ticketer/adapters/linear/types.py +80 -4
  18. mcp_ticketer/analysis/__init__.py +56 -0
  19. mcp_ticketer/analysis/dependency_graph.py +255 -0
  20. mcp_ticketer/analysis/health_assessment.py +304 -0
  21. mcp_ticketer/analysis/orphaned.py +218 -0
  22. mcp_ticketer/analysis/project_status.py +594 -0
  23. mcp_ticketer/analysis/similarity.py +224 -0
  24. mcp_ticketer/analysis/staleness.py +266 -0
  25. mcp_ticketer/automation/__init__.py +11 -0
  26. mcp_ticketer/automation/project_updates.py +378 -0
  27. mcp_ticketer/cache/memory.py +3 -3
  28. mcp_ticketer/cli/adapter_diagnostics.py +4 -2
  29. mcp_ticketer/cli/auggie_configure.py +18 -6
  30. mcp_ticketer/cli/codex_configure.py +175 -60
  31. mcp_ticketer/cli/configure.py +884 -146
  32. mcp_ticketer/cli/cursor_configure.py +314 -0
  33. mcp_ticketer/cli/diagnostics.py +31 -28
  34. mcp_ticketer/cli/discover.py +293 -21
  35. mcp_ticketer/cli/gemini_configure.py +18 -6
  36. mcp_ticketer/cli/init_command.py +880 -0
  37. mcp_ticketer/cli/instruction_commands.py +435 -0
  38. mcp_ticketer/cli/linear_commands.py +99 -15
  39. mcp_ticketer/cli/main.py +109 -2055
  40. mcp_ticketer/cli/mcp_configure.py +673 -99
  41. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  42. mcp_ticketer/cli/migrate_config.py +12 -8
  43. mcp_ticketer/cli/platform_commands.py +6 -6
  44. mcp_ticketer/cli/platform_detection.py +477 -0
  45. mcp_ticketer/cli/platform_installer.py +536 -0
  46. mcp_ticketer/cli/project_update_commands.py +350 -0
  47. mcp_ticketer/cli/queue_commands.py +15 -15
  48. mcp_ticketer/cli/setup_command.py +639 -0
  49. mcp_ticketer/cli/simple_health.py +13 -11
  50. mcp_ticketer/cli/ticket_commands.py +277 -36
  51. mcp_ticketer/cli/update_checker.py +313 -0
  52. mcp_ticketer/cli/utils.py +45 -41
  53. mcp_ticketer/core/__init__.py +35 -1
  54. mcp_ticketer/core/adapter.py +170 -5
  55. mcp_ticketer/core/config.py +38 -31
  56. mcp_ticketer/core/env_discovery.py +33 -3
  57. mcp_ticketer/core/env_loader.py +7 -6
  58. mcp_ticketer/core/exceptions.py +10 -4
  59. mcp_ticketer/core/http_client.py +10 -10
  60. mcp_ticketer/core/instructions.py +405 -0
  61. mcp_ticketer/core/label_manager.py +732 -0
  62. mcp_ticketer/core/mappers.py +32 -20
  63. mcp_ticketer/core/models.py +136 -1
  64. mcp_ticketer/core/onepassword_secrets.py +379 -0
  65. mcp_ticketer/core/priority_matcher.py +463 -0
  66. mcp_ticketer/core/project_config.py +148 -14
  67. mcp_ticketer/core/registry.py +1 -1
  68. mcp_ticketer/core/session_state.py +171 -0
  69. mcp_ticketer/core/state_matcher.py +592 -0
  70. mcp_ticketer/core/url_parser.py +425 -0
  71. mcp_ticketer/core/validators.py +69 -0
  72. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  73. mcp_ticketer/mcp/__init__.py +2 -2
  74. mcp_ticketer/mcp/server/__init__.py +2 -2
  75. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  76. mcp_ticketer/mcp/server/main.py +187 -93
  77. mcp_ticketer/mcp/server/routing.py +655 -0
  78. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  79. mcp_ticketer/mcp/server/tools/__init__.py +37 -9
  80. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  81. mcp_ticketer/mcp/server/tools/attachment_tools.py +65 -20
  82. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  83. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  84. mcp_ticketer/mcp/server/tools/config_tools.py +1429 -0
  85. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  86. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +878 -319
  87. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  88. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  89. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  90. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  91. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  92. mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
  93. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  94. mcp_ticketer/mcp/server/tools/ticket_tools.py +1182 -82
  95. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  96. mcp_ticketer/queue/health_monitor.py +1 -0
  97. mcp_ticketer/queue/manager.py +4 -4
  98. mcp_ticketer/queue/queue.py +3 -3
  99. mcp_ticketer/queue/run_worker.py +1 -1
  100. mcp_ticketer/queue/ticket_registry.py +2 -2
  101. mcp_ticketer/queue/worker.py +15 -13
  102. mcp_ticketer/utils/__init__.py +5 -0
  103. mcp_ticketer/utils/token_utils.py +246 -0
  104. mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
  105. mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
  106. mcp_ticketer-0.4.11.dist-info/METADATA +0 -496
  107. mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
  108. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
  109. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
  110. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
  111. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,246 @@
1
+ """Token counting and pagination utilities for MCP tool responses.
2
+
3
+ This module provides utilities for estimating token counts and implementing
4
+ token-aware pagination to ensure responses stay under 20k token limits.
5
+
6
+ Design Decision: Token estimation vs. exact counting
7
+ - Uses 4-chars-per-token heuristic (conservative)
8
+ - Rationale: Actual tokenization requires tiktoken library and GPT-specific
9
+ tokenizer, which adds dependency and runtime overhead
10
+ - Trade-off: Approximate (±10%) vs. exact, but fast and dependency-free
11
+ - Extension Point: Can add tiktoken support via optional dependency later
12
+
13
+ Performance: O(1) for token estimation (string length only)
14
+ Memory: O(1) auxiliary space (no allocations beyond JSON serialization)
15
+ """
16
+
17
+ import json
18
+ import logging
19
+ from collections.abc import Callable
20
+ from typing import Any, TypeVar
21
+
22
+ # Type variable for generic list items
23
+ T = TypeVar("T")
24
+
25
+ # Conservative token estimation: 1 token ≈ 4 characters
26
+ # Based on OpenAI/Anthropic averages for English text + JSON structure
27
+ CHARS_PER_TOKEN = 4
28
+
29
+ # Default maximum tokens per MCP response
30
+ DEFAULT_MAX_TOKENS = 20_000
31
+
32
+ # Overhead estimation for response metadata (status, adapter info, etc.)
33
+ BASE_RESPONSE_OVERHEAD = 100
34
+
35
+
36
+ def estimate_tokens(text: str) -> int:
37
+ """Estimate token count for a text string.
38
+
39
+ Uses conservative heuristic: 1 token ≈ 4 characters.
40
+ This works reasonably well for English text and JSON structures.
41
+
42
+ Design Trade-off:
43
+ - Fast: O(len(text)) string length check
44
+ - Approximate: ±10% accuracy vs. exact tokenization
45
+ - Zero dependencies: No tiktoken or model-specific tokenizers needed
46
+
47
+ Performance:
48
+ - Time Complexity: O(n) where n is string length
49
+ - Space Complexity: O(1)
50
+
51
+ Args:
52
+ text: Input text to estimate token count for
53
+
54
+ Returns:
55
+ Estimated token count (conservative, may overestimate slightly)
56
+
57
+ Example:
58
+ >>> estimate_tokens("Hello world")
59
+ 3 # "Hello world" = 11 chars / 4 ≈ 2.75 → rounds to 3
60
+ >>> estimate_tokens(json.dumps({"id": "123", "title": "Test"}))
61
+ 8 # JSON structure increases char count
62
+ """
63
+ if not text:
64
+ return 0
65
+ return max(1, len(text) // CHARS_PER_TOKEN)
66
+
67
+
68
+ def estimate_json_tokens(data: dict | list | Any) -> int:
69
+ """Estimate token count for JSON-serializable data.
70
+
71
+ Serializes data to JSON string then estimates tokens.
72
+ Accounts for JSON structure overhead (brackets, quotes, commas).
73
+
74
+ Performance:
75
+ - Time Complexity: O(n) where n is serialized JSON size
76
+ - Space Complexity: O(n) for JSON string (temporary)
77
+
78
+ Args:
79
+ data: Any JSON-serializable data (dict, list, primitives)
80
+
81
+ Returns:
82
+ Estimated token count for serialized representation
83
+
84
+ Example:
85
+ >>> estimate_json_tokens({"id": "123", "title": "Test"})
86
+ 8
87
+ >>> estimate_json_tokens([1, 2, 3])
88
+ 2
89
+ """
90
+ try:
91
+ json_str = json.dumps(data, default=str) # default=str for non-serializable
92
+ return estimate_tokens(json_str)
93
+ except (TypeError, ValueError) as e:
94
+ logging.warning(f"Failed to serialize data for token estimation: {e}")
95
+ # Fallback: estimate based on string representation
96
+ return estimate_tokens(str(data))
97
+
98
+
99
+ def paginate_response(
100
+ items: list[T],
101
+ limit: int = 20,
102
+ offset: int = 0,
103
+ max_tokens: int = DEFAULT_MAX_TOKENS,
104
+ serialize_fn: Callable[[T], dict] | None = None,
105
+ compact_fn: Callable[[dict], dict] | None = None,
106
+ compact: bool = True,
107
+ ) -> dict[str, Any]:
108
+ """Paginate a list of items with token-aware limiting.
109
+
110
+ This function implements automatic pagination that:
111
+ 1. Respects explicit limit/offset parameters
112
+ 2. Stops adding items if response would exceed max_tokens
113
+ 3. Optionally applies compact transformation to reduce token usage
114
+ 4. Returns pagination metadata for client-side handling
115
+
116
+ Design Decision: Token-aware vs. count-based pagination
117
+ - Hybrid approach: Uses both item count AND token limits
118
+ - Rationale: Prevents oversized responses even with small item counts
119
+ - Example: 10 tickets with huge descriptions could exceed 20k tokens
120
+ - Trade-off: Slightly more complex but safer for production use
121
+
122
+ Performance:
123
+ - Time Complexity: O(n) where n is min(limit, items until token limit)
124
+ - Space Complexity: O(n) for result items list
125
+ - Early termination: Stops as soon as token limit would be exceeded
126
+
127
+ Args:
128
+ items: List of items to paginate
129
+ limit: Maximum number of items to return (default: 20)
130
+ offset: Number of items to skip (default: 0)
131
+ max_tokens: Maximum tokens allowed in response (default: 20,000)
132
+ serialize_fn: Optional function to convert item to dict (e.g., model.model_dump)
133
+ compact_fn: Optional function to create compact representation
134
+ compact: Whether to apply compact_fn if provided (default: True)
135
+
136
+ Returns:
137
+ Dictionary containing:
138
+ - items: List of paginated items (serialized)
139
+ - count: Number of items returned
140
+ - total: Total items available (before pagination)
141
+ - offset: Offset used for this page
142
+ - limit: Limit requested
143
+ - has_more: Boolean indicating if more items exist
144
+ - truncated_by_tokens: Boolean indicating if token limit caused truncation
145
+ - estimated_tokens: Approximate token count for response
146
+
147
+ Error Conditions:
148
+ - Invalid limit (<= 0): Returns empty result with error flag
149
+ - Invalid offset (< 0): Uses offset=0
150
+ - serialize_fn fails: Logs warning and skips item
151
+
152
+ Example:
153
+ >>> tickets = [Ticket(...), Ticket(...), ...] # 100 tickets
154
+ >>> result = paginate_response(
155
+ ... tickets,
156
+ ... limit=20,
157
+ ... offset=0,
158
+ ... serialize_fn=lambda t: t.model_dump(),
159
+ ... compact_fn=_compact_ticket,
160
+ ... )
161
+ >>> result["count"] # 20 (or less if token limit hit)
162
+ >>> result["has_more"] # True
163
+ >>> result["estimated_tokens"] # ~2500
164
+ """
165
+ # Validate parameters
166
+ if limit <= 0:
167
+ logging.warning(f"Invalid limit {limit}, using default 20")
168
+ limit = 20
169
+
170
+ if offset < 0:
171
+ logging.warning(f"Invalid offset {offset}, using 0")
172
+ offset = 0
173
+
174
+ total_items = len(items)
175
+
176
+ # Apply offset
177
+ items_after_offset = items[offset:]
178
+
179
+ # Track token usage
180
+ estimated_tokens = BASE_RESPONSE_OVERHEAD # Base response overhead
181
+ result_items: list[dict] = []
182
+ truncated_by_tokens = False
183
+
184
+ # Process items up to limit or token threshold
185
+ for idx, item in enumerate(items_after_offset):
186
+ # Check if we've hit the limit
187
+ if idx >= limit:
188
+ break
189
+
190
+ # Serialize item
191
+ try:
192
+ if serialize_fn:
193
+ item_dict = serialize_fn(item)
194
+ elif hasattr(item, "model_dump"):
195
+ item_dict = item.model_dump()
196
+ elif isinstance(item, dict):
197
+ item_dict = item
198
+ else:
199
+ item_dict = {"data": str(item)}
200
+
201
+ # Apply compact mode if requested and function provided
202
+ if compact and compact_fn:
203
+ item_dict = compact_fn(item_dict)
204
+
205
+ # Estimate tokens for this item
206
+ item_tokens = estimate_json_tokens(item_dict)
207
+
208
+ # Check if adding this item would exceed token limit
209
+ if estimated_tokens + item_tokens > max_tokens:
210
+ logging.info(
211
+ f"Token limit reached: {estimated_tokens + item_tokens} > {max_tokens}. "
212
+ f"Returning {len(result_items)} items instead of requested {limit}."
213
+ )
214
+ truncated_by_tokens = True
215
+ break
216
+
217
+ # Add item to results
218
+ result_items.append(item_dict)
219
+ estimated_tokens += item_tokens
220
+
221
+ except Exception as e:
222
+ logging.warning(f"Failed to serialize item at index {idx + offset}: {e}")
223
+ # Skip this item and continue
224
+ continue
225
+
226
+ # Calculate pagination metadata
227
+ items_returned = len(result_items)
228
+ has_more = (offset + items_returned) < total_items
229
+
230
+ # Warn if approaching token limit
231
+ if estimated_tokens > max_tokens * 0.8:
232
+ logging.warning(
233
+ f"Response approaching token limit: {estimated_tokens}/{max_tokens} tokens "
234
+ f"({estimated_tokens/max_tokens*100:.1f}%). Consider using compact mode or reducing limit."
235
+ )
236
+
237
+ return {
238
+ "items": result_items,
239
+ "count": items_returned,
240
+ "total": total_items,
241
+ "offset": offset,
242
+ "limit": limit,
243
+ "has_more": has_more,
244
+ "truncated_by_tokens": truncated_by_tokens,
245
+ "estimated_tokens": estimated_tokens,
246
+ }