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

Potentially problematic release.


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

Files changed (84) hide show
  1. mcp_ticketer/__version__.py +3 -3
  2. mcp_ticketer/adapters/__init__.py +2 -0
  3. mcp_ticketer/adapters/aitrackdown.py +263 -14
  4. mcp_ticketer/adapters/asana/__init__.py +15 -0
  5. mcp_ticketer/adapters/asana/adapter.py +1308 -0
  6. mcp_ticketer/adapters/asana/client.py +292 -0
  7. mcp_ticketer/adapters/asana/mappers.py +334 -0
  8. mcp_ticketer/adapters/asana/types.py +146 -0
  9. mcp_ticketer/adapters/github.py +326 -109
  10. mcp_ticketer/adapters/hybrid.py +11 -11
  11. mcp_ticketer/adapters/jira.py +271 -25
  12. mcp_ticketer/adapters/linear/adapter.py +693 -39
  13. mcp_ticketer/adapters/linear/client.py +61 -9
  14. mcp_ticketer/adapters/linear/mappers.py +9 -3
  15. mcp_ticketer/adapters/linear/queries.py +9 -7
  16. mcp_ticketer/cache/memory.py +9 -8
  17. mcp_ticketer/cli/adapter_diagnostics.py +1 -1
  18. mcp_ticketer/cli/auggie_configure.py +104 -15
  19. mcp_ticketer/cli/codex_configure.py +188 -32
  20. mcp_ticketer/cli/configure.py +37 -48
  21. mcp_ticketer/cli/diagnostics.py +20 -18
  22. mcp_ticketer/cli/discover.py +292 -26
  23. mcp_ticketer/cli/gemini_configure.py +107 -26
  24. mcp_ticketer/cli/instruction_commands.py +429 -0
  25. mcp_ticketer/cli/linear_commands.py +105 -22
  26. mcp_ticketer/cli/main.py +1830 -435
  27. mcp_ticketer/cli/mcp_configure.py +296 -89
  28. mcp_ticketer/cli/migrate_config.py +12 -8
  29. mcp_ticketer/cli/platform_commands.py +123 -0
  30. mcp_ticketer/cli/platform_detection.py +412 -0
  31. mcp_ticketer/cli/python_detection.py +126 -0
  32. mcp_ticketer/cli/queue_commands.py +15 -15
  33. mcp_ticketer/cli/simple_health.py +1 -1
  34. mcp_ticketer/cli/ticket_commands.py +773 -0
  35. mcp_ticketer/cli/update_checker.py +313 -0
  36. mcp_ticketer/cli/utils.py +67 -62
  37. mcp_ticketer/core/__init__.py +14 -1
  38. mcp_ticketer/core/adapter.py +84 -15
  39. mcp_ticketer/core/config.py +44 -39
  40. mcp_ticketer/core/env_discovery.py +42 -12
  41. mcp_ticketer/core/env_loader.py +15 -14
  42. mcp_ticketer/core/exceptions.py +3 -3
  43. mcp_ticketer/core/http_client.py +26 -26
  44. mcp_ticketer/core/instructions.py +405 -0
  45. mcp_ticketer/core/mappers.py +11 -11
  46. mcp_ticketer/core/models.py +50 -20
  47. mcp_ticketer/core/onepassword_secrets.py +379 -0
  48. mcp_ticketer/core/project_config.py +57 -35
  49. mcp_ticketer/core/registry.py +3 -3
  50. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  51. mcp_ticketer/mcp/__init__.py +29 -1
  52. mcp_ticketer/mcp/__main__.py +60 -0
  53. mcp_ticketer/mcp/server/__init__.py +25 -0
  54. mcp_ticketer/mcp/server/__main__.py +60 -0
  55. mcp_ticketer/mcp/{dto.py → server/dto.py} +32 -32
  56. mcp_ticketer/mcp/{server.py → server/main.py} +127 -74
  57. mcp_ticketer/mcp/{response_builder.py → server/response_builder.py} +2 -2
  58. mcp_ticketer/mcp/server/server_sdk.py +93 -0
  59. mcp_ticketer/mcp/server/tools/__init__.py +47 -0
  60. mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
  61. mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
  62. mcp_ticketer/mcp/server/tools/comment_tools.py +90 -0
  63. mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
  64. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +532 -0
  65. mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
  66. mcp_ticketer/mcp/server/tools/pr_tools.py +154 -0
  67. mcp_ticketer/mcp/server/tools/search_tools.py +206 -0
  68. mcp_ticketer/mcp/server/tools/ticket_tools.py +430 -0
  69. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
  70. mcp_ticketer/queue/__init__.py +1 -0
  71. mcp_ticketer/queue/health_monitor.py +5 -4
  72. mcp_ticketer/queue/manager.py +15 -51
  73. mcp_ticketer/queue/queue.py +19 -19
  74. mcp_ticketer/queue/run_worker.py +1 -1
  75. mcp_ticketer/queue/ticket_registry.py +14 -14
  76. mcp_ticketer/queue/worker.py +16 -14
  77. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +168 -32
  78. mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
  79. mcp_ticketer-0.3.5.dist-info/RECORD +0 -62
  80. /mcp_ticketer/mcp/{constants.py → server/constants.py} +0 -0
  81. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
  82. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
  83. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
  84. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/top_level.txt +0 -0
@@ -69,7 +69,7 @@ class LinearGraphQLClient:
69
69
  return client
70
70
 
71
71
  except Exception as e:
72
- raise AdapterError(f"Failed to create Linear client: {e}", "linear")
72
+ raise AdapterError(f"Failed to create Linear client: {e}", "linear") from e
73
73
 
74
74
  async def execute_query(
75
75
  self,
@@ -110,15 +110,19 @@ class LinearGraphQLClient:
110
110
  status_code = e.response.status
111
111
 
112
112
  if status_code == 401:
113
- raise AuthenticationError("Invalid Linear API key", "linear")
113
+ raise AuthenticationError(
114
+ "Invalid Linear API key", "linear"
115
+ ) from e
114
116
  elif status_code == 403:
115
- raise AuthenticationError("Insufficient permissions", "linear")
117
+ raise AuthenticationError(
118
+ "Insufficient permissions", "linear"
119
+ ) from e
116
120
  elif status_code == 429:
117
121
  # Rate limit exceeded
118
122
  retry_after = e.response.headers.get("Retry-After", "60")
119
123
  raise RateLimitError(
120
124
  "Linear API rate limit exceeded", "linear", retry_after
121
- )
125
+ ) from e
122
126
  elif status_code >= 500:
123
127
  # Server error - retry
124
128
  if attempt < retries:
@@ -126,13 +130,13 @@ class LinearGraphQLClient:
126
130
  continue
127
131
  raise AdapterError(
128
132
  f"Linear API server error: {status_code}", "linear"
129
- )
133
+ ) from e
130
134
 
131
135
  # Network or other transport error
132
136
  if attempt < retries:
133
137
  await asyncio.sleep(2**attempt)
134
138
  continue
135
- raise AdapterError(f"Linear API transport error: {e}", "linear")
139
+ raise AdapterError(f"Linear API transport error: {e}", "linear") from e
136
140
 
137
141
  except Exception as e:
138
142
  # GraphQL or other errors
@@ -145,15 +149,19 @@ class LinearGraphQLClient:
145
149
  ):
146
150
  raise AuthenticationError(
147
151
  f"Linear authentication failed: {error_msg}", "linear"
148
- )
152
+ ) from e
149
153
  elif "rate limit" in error_msg.lower():
150
- raise RateLimitError("Linear API rate limit exceeded", "linear")
154
+ raise RateLimitError(
155
+ "Linear API rate limit exceeded", "linear"
156
+ ) from e
151
157
 
152
158
  # Generic error
153
159
  if attempt < retries:
154
160
  await asyncio.sleep(2**attempt)
155
161
  continue
156
- raise AdapterError(f"Linear GraphQL error: {error_msg}", "linear")
162
+ raise AdapterError(
163
+ f"Linear GraphQL error: {error_msg}", "linear"
164
+ ) from e
157
165
 
158
166
  # Should never reach here
159
167
  raise AdapterError("Maximum retries exceeded", "linear")
@@ -266,6 +274,50 @@ class LinearGraphQLClient:
266
274
  except Exception:
267
275
  return None
268
276
 
277
+ async def get_users_by_name(self, name: str) -> list[dict[str, Any]]:
278
+ """Search users by display name or full name.
279
+
280
+ Args:
281
+ name: Display name or full name to search for
282
+
283
+ Returns:
284
+ List of matching users (may be empty)
285
+
286
+ """
287
+ import logging
288
+
289
+ try:
290
+ query = """
291
+ query SearchUsers($nameFilter: String!) {
292
+ users(
293
+ filter: {
294
+ or: [
295
+ { displayName: { containsIgnoreCase: $nameFilter } }
296
+ { name: { containsIgnoreCase: $nameFilter } }
297
+ ]
298
+ }
299
+ first: 10
300
+ ) {
301
+ nodes {
302
+ id
303
+ name
304
+ email
305
+ displayName
306
+ avatarUrl
307
+ active
308
+ }
309
+ }
310
+ }
311
+ """
312
+
313
+ result = await self.execute_query(query, {"nameFilter": name})
314
+ users = result.get("users", {}).get("nodes", [])
315
+ return [u for u in users if u.get("active", True)] # Filter active users
316
+
317
+ except Exception as e:
318
+ logging.getLogger(__name__).warning(f"Failed to search users by name: {e}")
319
+ return []
320
+
269
321
  async def close(self) -> None:
270
322
  """Close the client connection.
271
323
 
@@ -10,7 +10,10 @@ from .types import extract_linear_metadata, get_universal_priority, get_universa
10
10
 
11
11
 
12
12
  def map_linear_issue_to_task(issue_data: dict[str, Any]) -> Task:
13
- """Convert Linear issue data to universal Task model.
13
+ """Convert Linear issue or sub-issue data to universal Task model.
14
+
15
+ Handles both top-level issues (no parent) and sub-issues (child items
16
+ with a parent issue).
14
17
 
15
18
  Args:
16
19
  issue_data: Raw Linear issue data from GraphQL
@@ -203,7 +206,10 @@ def map_linear_comment_to_comment(
203
206
 
204
207
 
205
208
  def build_linear_issue_input(task: Task, team_id: str) -> dict[str, Any]:
206
- """Build Linear issue input from universal Task model.
209
+ """Build Linear issue or sub-issue input from universal Task model.
210
+
211
+ Creates input for a top-level issue when task.parent_issue is not set,
212
+ or for a sub-issue when task.parent_issue is provided.
207
213
 
208
214
  Args:
209
215
  task: Universal Task model
@@ -215,7 +221,7 @@ def build_linear_issue_input(task: Task, team_id: str) -> dict[str, Any]:
215
221
  """
216
222
  from .types import get_linear_priority
217
223
 
218
- issue_input = {
224
+ issue_input: dict[str, Any] = {
219
225
  "title": task.title,
220
226
  "teamId": team_id,
221
227
  }
@@ -226,13 +226,15 @@ ISSUE_LIST_FRAGMENTS = (
226
226
 
227
227
  WORKFLOW_STATES_QUERY = """
228
228
  query WorkflowStates($teamId: String!) {
229
- workflowStates(filter: { team: { id: { eq: $teamId } } }) {
230
- nodes {
231
- id
232
- name
233
- type
234
- position
235
- color
229
+ team(id: $teamId) {
230
+ states {
231
+ nodes {
232
+ id
233
+ name
234
+ type
235
+ position
236
+ color
237
+ }
236
238
  }
237
239
  }
238
240
  }
@@ -4,8 +4,9 @@ import asyncio
4
4
  import hashlib
5
5
  import json
6
6
  import time
7
+ from collections.abc import Callable
7
8
  from functools import wraps
8
- from typing import Any, Callable, Optional
9
+ from typing import Any
9
10
 
10
11
 
11
12
  class CacheEntry:
@@ -41,7 +42,7 @@ class MemoryCache:
41
42
  self._default_ttl = default_ttl
42
43
  self._lock = asyncio.Lock()
43
44
 
44
- async def get(self, key: str) -> Optional[Any]:
45
+ async def get(self, key: str) -> Any | None:
45
46
  """Get value from cache.
46
47
 
47
48
  Args:
@@ -60,7 +61,7 @@ class MemoryCache:
60
61
  del self._cache[key]
61
62
  return None
62
63
 
63
- async def set(self, key: str, value: Any, ttl: Optional[float] = None) -> None:
64
+ async def set(self, key: str, value: Any, ttl: float | None = None) -> None:
64
65
  """Set value in cache.
65
66
 
66
67
  Args:
@@ -114,7 +115,7 @@ class MemoryCache:
114
115
  return len(self._cache)
115
116
 
116
117
  @staticmethod
117
- def generate_key(*args, **kwargs) -> str:
118
+ def generate_key(*args: Any, **kwargs: Any) -> str:
118
119
  """Generate cache key from arguments.
119
120
 
120
121
  Args:
@@ -134,11 +135,11 @@ class MemoryCache:
134
135
 
135
136
 
136
137
  def cache_decorator(
137
- ttl: Optional[float] = None,
138
+ ttl: float | None = None,
138
139
  key_prefix: str = "",
139
- cache_instance: Optional[MemoryCache] = None,
140
+ cache_instance: MemoryCache | None = None,
140
141
  ) -> Callable:
141
- """Decorator for caching async function results.
142
+ """Decorate async function to cache its results.
142
143
 
143
144
  Args:
144
145
  ttl: TTL for cached results
@@ -154,7 +155,7 @@ def cache_decorator(
154
155
 
155
156
  def decorator(func: Callable) -> Callable:
156
157
  @wraps(func)
157
- async def wrapper(*args, **kwargs):
158
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
158
159
  # Generate cache key
159
160
  base_key = MemoryCache.generate_key(*args, **kwargs)
160
161
  cache_key = f"{key_prefix}:{func.__name__}:{base_key}"
@@ -343,7 +343,7 @@ def _provide_recommendations(console: Console) -> None:
343
343
  console.print("• For local files: [cyan]mcp-ticketer init aitrackdown[/cyan]")
344
344
 
345
345
  console.print("\n[bold]Test Configuration:[/bold]")
346
- console.print("• Run diagnostics: [cyan]mcp-ticketer diagnose[/cyan]")
346
+ console.print("• Run diagnostics: [cyan]mcp-ticketer doctor[/cyan]")
347
347
  console.print(
348
348
  "• Test ticket creation: [cyan]mcp-ticketer create 'Test ticket'[/cyan]"
349
349
  )
@@ -10,7 +10,8 @@ from typing import Any
10
10
 
11
11
  from rich.console import Console
12
12
 
13
- from .mcp_configure import find_mcp_ticketer_binary, load_project_config
13
+ from .mcp_configure import load_project_config
14
+ from .python_detection import get_mcp_ticketer_python
14
15
 
15
16
  console = Console()
16
17
 
@@ -71,18 +72,22 @@ def save_auggie_config(config_path: Path, config: dict[str, Any]) -> None:
71
72
 
72
73
 
73
74
  def create_auggie_server_config(
74
- binary_path: str, project_config: dict[str, Any]
75
+ python_path: str, project_config: dict[str, Any], project_path: str | None = None
75
76
  ) -> dict[str, Any]:
76
77
  """Create Auggie MCP server configuration for mcp-ticketer.
77
78
 
78
79
  Args:
79
- binary_path: Path to mcp-ticketer binary
80
+ python_path: Path to Python executable in mcp-ticketer venv
80
81
  project_config: Project configuration from .mcp-ticketer/config.json
82
+ project_path: Project directory path (optional)
81
83
 
82
84
  Returns:
83
85
  Auggie MCP server configuration dict
84
86
 
85
87
  """
88
+ # Use Python module invocation pattern (works regardless of where package is installed)
89
+ from pathlib import Path
90
+
86
91
  # Get adapter configuration
87
92
  adapter = project_config.get("default_adapter", "aitrackdown")
88
93
  adapters_config = project_config.get("adapters", {})
@@ -91,6 +96,10 @@ def create_auggie_server_config(
91
96
  # Build environment variables
92
97
  env_vars = {}
93
98
 
99
+ # Add PYTHONPATH for project context
100
+ if project_path:
101
+ env_vars["PYTHONPATH"] = project_path
102
+
94
103
  # Add adapter type
95
104
  env_vars["MCP_TICKETER_ADAPTER"] = adapter
96
105
 
@@ -128,16 +137,87 @@ def create_auggie_server_config(
128
137
  if "project_key" in adapter_config:
129
138
  env_vars["JIRA_PROJECT_KEY"] = adapter_config["project_key"]
130
139
 
140
+ # Use Python module invocation pattern
141
+ args = ["-m", "mcp_ticketer.mcp.server"]
142
+ if project_path:
143
+ args.append(project_path)
144
+
131
145
  # Create server configuration (simpler than Gemini - no timeout/trust)
132
146
  config = {
133
- "command": binary_path,
134
- "args": ["serve"],
147
+ "command": python_path,
148
+ "args": args,
135
149
  "env": env_vars,
136
150
  }
137
151
 
138
152
  return config
139
153
 
140
154
 
155
+ def remove_auggie_mcp(dry_run: bool = False) -> None:
156
+ """Remove mcp-ticketer from Auggie CLI configuration.
157
+
158
+ IMPORTANT: Auggie CLI ONLY supports global configuration.
159
+ This will remove mcp-ticketer from ~/.augment/settings.json.
160
+
161
+ Args:
162
+ dry_run: Show what would be removed without making changes
163
+
164
+ """
165
+ # Step 1: Find Auggie config location
166
+ console.print("[cyan]🔍 Removing Auggie CLI global configuration...[/cyan]")
167
+ console.print(
168
+ "[yellow]⚠ NOTE: Auggie only supports global configuration (affects all projects)[/yellow]"
169
+ )
170
+
171
+ auggie_config_path = find_auggie_config()
172
+ console.print(f"[dim]Config location: {auggie_config_path}[/dim]")
173
+
174
+ # Step 2: Check if config file exists
175
+ if not auggie_config_path.exists():
176
+ console.print(
177
+ f"[yellow]⚠ No configuration found at {auggie_config_path}[/yellow]"
178
+ )
179
+ console.print("[dim]mcp-ticketer is not configured for Auggie[/dim]")
180
+ return
181
+
182
+ # Step 3: Load existing Auggie configuration
183
+ auggie_config = load_auggie_config(auggie_config_path)
184
+
185
+ # Step 4: Check if mcp-ticketer is configured
186
+ if "mcp-ticketer" not in auggie_config.get("mcpServers", {}):
187
+ console.print("[yellow]⚠ mcp-ticketer is not configured[/yellow]")
188
+ console.print(f"[dim]No mcp-ticketer entry found in {auggie_config_path}[/dim]")
189
+ return
190
+
191
+ # Step 5: Show what would be removed (dry run or actual removal)
192
+ if dry_run:
193
+ console.print("\n[cyan]DRY RUN - Would remove:[/cyan]")
194
+ console.print(" Server name: mcp-ticketer")
195
+ console.print(f" From: {auggie_config_path}")
196
+ console.print(" Scope: Global (all projects)")
197
+ return
198
+
199
+ # Step 6: Remove mcp-ticketer from configuration
200
+ del auggie_config["mcpServers"]["mcp-ticketer"]
201
+
202
+ # Step 7: Save updated configuration
203
+ try:
204
+ save_auggie_config(auggie_config_path, auggie_config)
205
+ console.print("\n[green]✓ Successfully removed mcp-ticketer[/green]")
206
+ console.print(f"[dim]Configuration updated: {auggie_config_path}[/dim]")
207
+
208
+ # Next steps
209
+ console.print("\n[bold cyan]Next Steps:[/bold cyan]")
210
+ console.print("1. Restart Auggie CLI for changes to take effect")
211
+ console.print("2. mcp-ticketer will no longer be available via MCP")
212
+ console.print(
213
+ "\n[yellow]⚠ Note: This removes global configuration affecting all projects[/yellow]"
214
+ )
215
+
216
+ except Exception as e:
217
+ console.print(f"\n[red]✗ Failed to update configuration:[/red] {e}")
218
+ raise
219
+
220
+
141
221
  def configure_auggie_mcp(force: bool = False) -> None:
142
222
  """Configure Auggie CLI to use mcp-ticketer.
143
223
 
@@ -148,18 +228,22 @@ def configure_auggie_mcp(force: bool = False) -> None:
148
228
  force: Overwrite existing configuration
149
229
 
150
230
  Raises:
151
- FileNotFoundError: If binary or project config not found
231
+ FileNotFoundError: If Python executable or project config not found
152
232
  ValueError: If configuration is invalid
153
233
 
154
234
  """
155
- # Step 1: Find mcp-ticketer binary
156
- console.print("[cyan]🔍 Finding mcp-ticketer binary...[/cyan]")
235
+ # Step 1: Find Python executable
236
+ console.print("[cyan]🔍 Finding mcp-ticketer Python executable...[/cyan]")
157
237
  try:
158
- binary_path = find_mcp_ticketer_binary()
159
- console.print(f"[green]✓[/green] Found: {binary_path}")
160
- except FileNotFoundError as e:
161
- console.print(f"[red]✗[/red] {e}")
162
- raise
238
+ python_path = get_mcp_ticketer_python()
239
+ console.print(f"[green]✓[/green] Found: {python_path}")
240
+ except Exception as e:
241
+ console.print(f"[red]✗[/red] Could not find Python executable: {e}")
242
+ raise FileNotFoundError(
243
+ "Could not find mcp-ticketer Python executable. "
244
+ "Please ensure mcp-ticketer is installed.\n"
245
+ "Install with: pip install mcp-ticketer or pipx install mcp-ticketer"
246
+ ) from e
163
247
 
164
248
  # Step 2: Load project configuration
165
249
  console.print("\n[cyan]📖 Reading project configuration...[/cyan]")
@@ -193,8 +277,11 @@ def configure_auggie_mcp(force: bool = False) -> None:
193
277
  console.print("[yellow]⚠ Overwriting existing configuration[/yellow]")
194
278
 
195
279
  # Step 6: Create mcp-ticketer server config
280
+ project_path = str(Path.cwd())
196
281
  server_config = create_auggie_server_config(
197
- binary_path=binary_path, project_config=project_config
282
+ python_path=python_path,
283
+ project_config=project_config,
284
+ project_path=project_path,
198
285
  )
199
286
 
200
287
  # Step 7: Update Auggie configuration
@@ -213,8 +300,10 @@ def configure_auggie_mcp(force: bool = False) -> None:
213
300
  console.print("\n[bold]Configuration Details:[/bold]")
214
301
  console.print(" Server name: mcp-ticketer")
215
302
  console.print(f" Adapter: {adapter}")
216
- console.print(f" Binary: {binary_path}")
303
+ console.print(f" Python: {python_path}")
304
+ console.print(" Command: python -m mcp_ticketer.mcp.server")
217
305
  console.print(" Scope: Global (affects all projects)")
306
+ console.print(f" Project path: {project_path}")
218
307
  if "env" in server_config:
219
308
  console.print(
220
309
  f" Environment variables: {list(server_config['env'].keys())}"