mcp-ticketer 0.4.1__py3-none-any.whl → 0.4.3__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 (56) hide show
  1. mcp_ticketer/__init__.py +3 -12
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/adapters/aitrackdown.py +243 -11
  4. mcp_ticketer/adapters/github.py +15 -14
  5. mcp_ticketer/adapters/hybrid.py +11 -11
  6. mcp_ticketer/adapters/jira.py +22 -25
  7. mcp_ticketer/adapters/linear/adapter.py +9 -21
  8. mcp_ticketer/adapters/linear/client.py +2 -1
  9. mcp_ticketer/adapters/linear/mappers.py +2 -1
  10. mcp_ticketer/cache/memory.py +6 -5
  11. mcp_ticketer/cli/adapter_diagnostics.py +4 -2
  12. mcp_ticketer/cli/auggie_configure.py +66 -0
  13. mcp_ticketer/cli/codex_configure.py +70 -2
  14. mcp_ticketer/cli/configure.py +7 -14
  15. mcp_ticketer/cli/diagnostics.py +2 -2
  16. mcp_ticketer/cli/discover.py +6 -11
  17. mcp_ticketer/cli/gemini_configure.py +68 -2
  18. mcp_ticketer/cli/linear_commands.py +6 -7
  19. mcp_ticketer/cli/main.py +341 -203
  20. mcp_ticketer/cli/mcp_configure.py +61 -2
  21. mcp_ticketer/cli/ticket_commands.py +27 -30
  22. mcp_ticketer/cli/utils.py +23 -22
  23. mcp_ticketer/core/__init__.py +3 -1
  24. mcp_ticketer/core/adapter.py +82 -13
  25. mcp_ticketer/core/config.py +27 -29
  26. mcp_ticketer/core/env_discovery.py +10 -10
  27. mcp_ticketer/core/env_loader.py +8 -8
  28. mcp_ticketer/core/http_client.py +16 -16
  29. mcp_ticketer/core/mappers.py +10 -10
  30. mcp_ticketer/core/models.py +50 -20
  31. mcp_ticketer/core/project_config.py +40 -34
  32. mcp_ticketer/core/registry.py +2 -2
  33. mcp_ticketer/mcp/dto.py +32 -32
  34. mcp_ticketer/mcp/response_builder.py +2 -2
  35. mcp_ticketer/mcp/server.py +17 -37
  36. mcp_ticketer/mcp/server_sdk.py +93 -0
  37. mcp_ticketer/mcp/tools/__init__.py +36 -0
  38. mcp_ticketer/mcp/tools/attachment_tools.py +179 -0
  39. mcp_ticketer/mcp/tools/bulk_tools.py +273 -0
  40. mcp_ticketer/mcp/tools/comment_tools.py +90 -0
  41. mcp_ticketer/mcp/tools/hierarchy_tools.py +383 -0
  42. mcp_ticketer/mcp/tools/pr_tools.py +154 -0
  43. mcp_ticketer/mcp/tools/search_tools.py +206 -0
  44. mcp_ticketer/mcp/tools/ticket_tools.py +277 -0
  45. mcp_ticketer/queue/health_monitor.py +4 -4
  46. mcp_ticketer/queue/manager.py +2 -2
  47. mcp_ticketer/queue/queue.py +16 -16
  48. mcp_ticketer/queue/ticket_registry.py +7 -7
  49. mcp_ticketer/queue/worker.py +2 -2
  50. {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/METADATA +90 -17
  51. mcp_ticketer-0.4.3.dist-info/RECORD +73 -0
  52. mcp_ticketer-0.4.1.dist-info/RECORD +0 -64
  53. {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/WHEEL +0 -0
  54. {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/entry_points.txt +0 -0
  55. {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/licenses/LICENSE +0 -0
  56. {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/top_level.txt +0 -0
@@ -6,20 +6,21 @@ import logging
6
6
  import re
7
7
  from datetime import datetime
8
8
  from enum import Enum
9
- from typing import Any, Optional, Union
9
+ from typing import Any, Union
10
10
 
11
11
  import httpx
12
12
  from httpx import AsyncClient, HTTPStatusError, TimeoutException
13
13
 
14
14
  from ..core.adapter import BaseAdapter
15
15
  from ..core.env_loader import load_adapter_config, validate_adapter_config
16
- from ..core.models import Comment, Epic, Priority, SearchQuery, Task, TicketState
16
+ from ..core.models import (Comment, Epic, Priority, SearchQuery, Task,
17
+ TicketState)
17
18
  from ..core.registry import AdapterRegistry
18
19
 
19
20
  logger = logging.getLogger(__name__)
20
21
 
21
22
 
22
- def parse_jira_datetime(date_str: str) -> Optional[datetime]:
23
+ def parse_jira_datetime(date_str: str) -> datetime | None:
23
24
  """Parse JIRA datetime strings which can be in various formats.
24
25
 
25
26
  JIRA can return dates in formats like:
@@ -47,7 +48,7 @@ def parse_jira_datetime(date_str: str) -> Optional[datetime]:
47
48
  return None
48
49
 
49
50
 
50
- def extract_text_from_adf(adf_content: Union[str, dict[str, Any]]) -> str:
51
+ def extract_text_from_adf(adf_content: str | dict[str, Any]) -> str:
51
52
  """Extract plain text from Atlassian Document Format (ADF).
52
53
 
53
54
  Args:
@@ -221,8 +222,8 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
221
222
  self,
222
223
  method: str,
223
224
  endpoint: str,
224
- data: Optional[dict[str, Any]] = None,
225
- params: Optional[dict[str, Any]] = None,
225
+ data: dict[str, Any] | None = None,
226
+ params: dict[str, Any] | None = None,
226
227
  retry_count: int = 0,
227
228
  ) -> dict[str, Any]:
228
229
  """Make HTTP request to JIRA API with retry logic.
@@ -287,7 +288,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
287
288
  return self._priority_cache
288
289
 
289
290
  async def _get_issue_types(
290
- self, project_key: Optional[str] = None
291
+ self, project_key: str | None = None
291
292
  ) -> list[dict[str, Any]]:
292
293
  """Get available issue types for a project."""
293
294
  key = project_key or self.project_key
@@ -380,9 +381,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
380
381
  }
381
382
  return mapping.get(priority, JiraPriority.MEDIUM)
382
383
 
383
- def _map_priority_from_jira(
384
- self, jira_priority: Optional[dict[str, Any]]
385
- ) -> Priority:
384
+ def _map_priority_from_jira(self, jira_priority: dict[str, Any] | None) -> Priority:
386
385
  """Map JIRA priority to universal priority."""
387
386
  if not jira_priority:
388
387
  return Priority.MEDIUM
@@ -432,7 +431,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
432
431
  else:
433
432
  return TicketState.OPEN
434
433
 
435
- def _issue_to_ticket(self, issue: dict[str, Any]) -> Union[Epic, Task]:
434
+ def _issue_to_ticket(self, issue: dict[str, Any]) -> Epic | Task:
436
435
  """Convert JIRA issue to universal ticket model."""
437
436
  fields = issue.get("fields", {})
438
437
 
@@ -507,7 +506,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
507
506
  )
508
507
 
509
508
  def _ticket_to_issue_fields(
510
- self, ticket: Union[Epic, Task], issue_type: Optional[str] = None
509
+ self, ticket: Epic | Task, issue_type: str | None = None
511
510
  ) -> dict[str, Any]:
512
511
  """Convert universal ticket to JIRA issue fields."""
513
512
  # Convert description to ADF format for JIRA Cloud
@@ -556,7 +555,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
556
555
 
557
556
  return fields
558
557
 
559
- async def create(self, ticket: Union[Epic, Task]) -> Union[Epic, Task]:
558
+ async def create(self, ticket: Epic | Task) -> Epic | Task:
560
559
  """Create a new JIRA issue."""
561
560
  # Validate credentials before attempting operation
562
561
  is_valid, error_message = self.validate_credentials()
@@ -576,7 +575,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
576
575
  created_issue = await self._make_request("GET", f"issue/{ticket.id}")
577
576
  return self._issue_to_ticket(created_issue)
578
577
 
579
- async def read(self, ticket_id: str) -> Optional[Union[Epic, Task]]:
578
+ async def read(self, ticket_id: str) -> Epic | Task | None:
580
579
  """Read a JIRA issue by key."""
581
580
  # Validate credentials before attempting operation
582
581
  is_valid, error_message = self.validate_credentials()
@@ -595,7 +594,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
595
594
 
596
595
  async def update(
597
596
  self, ticket_id: str, updates: dict[str, Any]
598
- ) -> Optional[Union[Epic, Task]]:
597
+ ) -> Epic | Task | None:
599
598
  """Update a JIRA issue."""
600
599
  # Validate credentials before attempting operation
601
600
  is_valid, error_message = self.validate_credentials()
@@ -652,8 +651,8 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
652
651
  raise
653
652
 
654
653
  async def list(
655
- self, limit: int = 10, offset: int = 0, filters: Optional[dict[str, Any]] = None
656
- ) -> list[Union[Epic, Task]]:
654
+ self, limit: int = 10, offset: int = 0, filters: dict[str, Any] | None = None
655
+ ) -> list[Epic | Task]:
657
656
  """List JIRA issues with pagination."""
658
657
  # Build JQL query
659
658
  jql_parts = []
@@ -692,7 +691,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
692
691
  issues = data.get("issues", [])
693
692
  return [self._issue_to_ticket(issue) for issue in issues]
694
693
 
695
- async def search(self, query: SearchQuery) -> builtins.list[Union[Epic, Task]]:
694
+ async def search(self, query: SearchQuery) -> builtins.list[Epic | Task]:
696
695
  """Search JIRA issues using JQL."""
697
696
  # Build JQL query
698
697
  jql_parts = []
@@ -744,7 +743,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
744
743
 
745
744
  async def transition_state(
746
745
  self, ticket_id: str, target_state: TicketState
747
- ) -> Optional[Union[Epic, Task]]:
746
+ ) -> Epic | Task | None:
748
747
  """Transition JIRA issue to a new state."""
749
748
  # Get available transitions
750
749
  transitions = await self._get_transitions(ticket_id)
@@ -858,9 +857,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
858
857
 
859
858
  return comments
860
859
 
861
- async def get_project_info(
862
- self, project_key: Optional[str] = None
863
- ) -> dict[str, Any]:
860
+ async def get_project_info(self, project_key: str | None = None) -> dict[str, Any]:
864
861
  """Get JIRA project information including workflows and fields."""
865
862
  key = project_key or self.project_key
866
863
  if not key:
@@ -882,7 +879,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
882
879
 
883
880
  async def execute_jql(
884
881
  self, jql: str, limit: int = 50
885
- ) -> builtins.list[Union[Epic, Task]]:
882
+ ) -> builtins.list[Epic | Task]:
886
883
  """Execute a raw JQL query.
887
884
 
888
885
  Args:
@@ -908,7 +905,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
908
905
  return [self._issue_to_ticket(issue) for issue in issues]
909
906
 
910
907
  async def get_sprints(
911
- self, board_id: Optional[int] = None
908
+ self, board_id: int | None = None
912
909
  ) -> builtins.list[dict[str, Any]]:
913
910
  """Get active sprints for a board (requires JIRA Software).
914
911
 
@@ -992,7 +989,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
992
989
  except Exception:
993
990
  return []
994
991
 
995
- async def get_current_user(self) -> Optional[dict[str, Any]]:
992
+ async def get_current_user(self) -> dict[str, Any] | None:
996
993
  """Get current authenticated user information."""
997
994
  try:
998
995
  return await self._make_request("GET", "myself")
@@ -20,27 +20,15 @@ from ...core.adapter import BaseAdapter
20
20
  from ...core.models import Comment, Epic, SearchQuery, Task, TicketState
21
21
  from ...core.registry import AdapterRegistry
22
22
  from .client import LinearGraphQLClient
23
- from .mappers import (
24
- build_linear_issue_input,
25
- build_linear_issue_update_input,
26
- map_linear_comment_to_comment,
27
- map_linear_issue_to_task,
28
- map_linear_project_to_epic,
29
- )
30
- from .queries import (
31
- ALL_FRAGMENTS,
32
- CREATE_ISSUE_MUTATION,
33
- LIST_ISSUES_QUERY,
34
- SEARCH_ISSUES_QUERY,
35
- UPDATE_ISSUE_MUTATION,
36
- WORKFLOW_STATES_QUERY,
37
- )
38
- from .types import (
39
- LinearStateMapping,
40
- build_issue_filter,
41
- get_linear_priority,
42
- get_linear_state_type,
43
- )
23
+ from .mappers import (build_linear_issue_input,
24
+ build_linear_issue_update_input,
25
+ map_linear_comment_to_comment, map_linear_issue_to_task,
26
+ map_linear_project_to_epic)
27
+ from .queries import (ALL_FRAGMENTS, CREATE_ISSUE_MUTATION, LIST_ISSUES_QUERY,
28
+ SEARCH_ISSUES_QUERY, UPDATE_ISSUE_MUTATION,
29
+ WORKFLOW_STATES_QUERY)
30
+ from .types import (LinearStateMapping, build_issue_filter,
31
+ get_linear_priority, get_linear_state_type)
44
32
 
45
33
 
46
34
  class LinearAdapter(BaseAdapter[Task]):
@@ -16,7 +16,8 @@ except ImportError:
16
16
  HTTPXAsyncTransport = None
17
17
  TransportError = Exception
18
18
 
19
- from ...core.exceptions import AdapterError, AuthenticationError, RateLimitError
19
+ from ...core.exceptions import (AdapterError, AuthenticationError,
20
+ RateLimitError)
20
21
 
21
22
 
22
23
  class LinearGraphQLClient:
@@ -6,7 +6,8 @@ from datetime import datetime
6
6
  from typing import Any
7
7
 
8
8
  from ...core.models import Comment, Epic, Priority, Task, TicketState
9
- from .types import extract_linear_metadata, get_universal_priority, get_universal_state
9
+ from .types import (extract_linear_metadata, get_universal_priority,
10
+ get_universal_state)
10
11
 
11
12
 
12
13
  def map_linear_issue_to_task(issue_data: dict[str, Any]) -> Task:
@@ -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:
@@ -134,9 +135,9 @@ 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
142
  """Decorator for caching async function results.
142
143
 
@@ -197,7 +197,8 @@ def _test_adapter_instantiation(console: Console) -> None:
197
197
  if primary:
198
198
  adapter_type = primary.adapter_type
199
199
  # Build config from discovery
200
- from ..mcp.server import _build_adapter_config_from_env_vars
200
+ from ..mcp.server import \
201
+ _build_adapter_config_from_env_vars
201
202
 
202
203
  config = _build_adapter_config_from_env_vars(adapter_type, {})
203
204
  else:
@@ -384,7 +385,8 @@ def get_adapter_status() -> dict[str, Any]:
384
385
  adapter_type = primary.adapter_type
385
386
  status["configuration_source"] = primary.found_in
386
387
  # Build basic config
387
- from ..mcp.server import _build_adapter_config_from_env_vars
388
+ from ..mcp.server import \
389
+ _build_adapter_config_from_env_vars
388
390
 
389
391
  config = _build_adapter_config_from_env_vars(adapter_type, {})
390
392
  else:
@@ -138,6 +138,72 @@ def create_auggie_server_config(
138
138
  return config
139
139
 
140
140
 
141
+ def remove_auggie_mcp(dry_run: bool = False) -> None:
142
+ """Remove mcp-ticketer from Auggie CLI configuration.
143
+
144
+ IMPORTANT: Auggie CLI ONLY supports global configuration.
145
+ This will remove mcp-ticketer from ~/.augment/settings.json.
146
+
147
+ Args:
148
+ dry_run: Show what would be removed without making changes
149
+
150
+ """
151
+ # Step 1: Find Auggie config location
152
+ console.print("[cyan]🔍 Removing Auggie CLI global configuration...[/cyan]")
153
+ console.print(
154
+ "[yellow]⚠ NOTE: Auggie only supports global configuration (affects all projects)[/yellow]"
155
+ )
156
+
157
+ auggie_config_path = find_auggie_config()
158
+ console.print(f"[dim]Config location: {auggie_config_path}[/dim]")
159
+
160
+ # Step 2: Check if config file exists
161
+ if not auggie_config_path.exists():
162
+ console.print(
163
+ f"[yellow]⚠ No configuration found at {auggie_config_path}[/yellow]"
164
+ )
165
+ console.print("[dim]mcp-ticketer is not configured for Auggie[/dim]")
166
+ return
167
+
168
+ # Step 3: Load existing Auggie configuration
169
+ auggie_config = load_auggie_config(auggie_config_path)
170
+
171
+ # Step 4: Check if mcp-ticketer is configured
172
+ if "mcp-ticketer" not in auggie_config.get("mcpServers", {}):
173
+ console.print("[yellow]⚠ mcp-ticketer is not configured[/yellow]")
174
+ console.print(f"[dim]No mcp-ticketer entry found in {auggie_config_path}[/dim]")
175
+ return
176
+
177
+ # Step 5: Show what would be removed (dry run or actual removal)
178
+ if dry_run:
179
+ console.print("\n[cyan]DRY RUN - Would remove:[/cyan]")
180
+ console.print(" Server name: mcp-ticketer")
181
+ console.print(f" From: {auggie_config_path}")
182
+ console.print(" Scope: Global (all projects)")
183
+ return
184
+
185
+ # Step 6: Remove mcp-ticketer from configuration
186
+ del auggie_config["mcpServers"]["mcp-ticketer"]
187
+
188
+ # Step 7: Save updated configuration
189
+ try:
190
+ save_auggie_config(auggie_config_path, auggie_config)
191
+ console.print("\n[green]✓ Successfully removed mcp-ticketer[/green]")
192
+ console.print(f"[dim]Configuration updated: {auggie_config_path}[/dim]")
193
+
194
+ # Next steps
195
+ console.print("\n[bold cyan]Next Steps:[/bold cyan]")
196
+ console.print("1. Restart Auggie CLI for changes to take effect")
197
+ console.print("2. mcp-ticketer will no longer be available via MCP")
198
+ console.print(
199
+ "\n[yellow]⚠ Note: This removes global configuration affecting all projects[/yellow]"
200
+ )
201
+
202
+ except Exception as e:
203
+ console.print(f"\n[red]✗ Failed to update configuration:[/red] {e}")
204
+ raise
205
+
206
+
141
207
  def configure_auggie_mcp(force: bool = False) -> None:
142
208
  """Configure Auggie CLI to use mcp-ticketer.
143
209
 
@@ -6,7 +6,7 @@ Unlike Claude Code and Gemini CLI, there is no project-level configuration suppo
6
6
 
7
7
  import sys
8
8
  from pathlib import Path
9
- from typing import Any, Optional
9
+ from typing import Any
10
10
 
11
11
  if sys.version_info >= (3, 11):
12
12
  import tomllib
@@ -78,7 +78,7 @@ def save_codex_config(config_path: Path, config: dict[str, Any]) -> None:
78
78
 
79
79
 
80
80
  def create_codex_server_config(
81
- binary_path: str, project_config: dict, cwd: Optional[str] = None
81
+ binary_path: str, project_config: dict, cwd: str | None = None
82
82
  ) -> dict[str, Any]:
83
83
  """Create Codex MCP server configuration for mcp-ticketer.
84
84
 
@@ -151,6 +151,74 @@ def create_codex_server_config(
151
151
  return config
152
152
 
153
153
 
154
+ def remove_codex_mcp(dry_run: bool = False) -> None:
155
+ """Remove mcp-ticketer from Codex CLI configuration.
156
+
157
+ IMPORTANT: Codex CLI ONLY supports global configuration at ~/.codex/config.toml.
158
+ This will remove mcp-ticketer from the global configuration.
159
+
160
+ Args:
161
+ dry_run: Show what would be removed without making changes
162
+
163
+ """
164
+ # Step 1: Find Codex config location (always global)
165
+ console.print("[cyan]🔍 Removing Codex CLI global configuration...[/cyan]")
166
+ console.print(
167
+ "[yellow]⚠ Note: Codex CLI only supports global configuration[/yellow]"
168
+ )
169
+
170
+ codex_config_path = find_codex_config()
171
+ console.print(f"[dim]Config location: {codex_config_path}[/dim]")
172
+
173
+ # Step 2: Check if config file exists
174
+ if not codex_config_path.exists():
175
+ console.print(
176
+ f"[yellow]⚠ No configuration found at {codex_config_path}[/yellow]"
177
+ )
178
+ console.print("[dim]mcp-ticketer is not configured for Codex CLI[/dim]")
179
+ return
180
+
181
+ # Step 3: Load existing Codex configuration
182
+ codex_config = load_codex_config(codex_config_path)
183
+
184
+ # Step 4: Check if mcp-ticketer is configured
185
+ # NOTE: Use underscore mcp_servers, not camelCase
186
+ mcp_servers = codex_config.get("mcp_servers", {})
187
+ if "mcp-ticketer" not in mcp_servers:
188
+ console.print("[yellow]⚠ mcp-ticketer is not configured[/yellow]")
189
+ console.print(f"[dim]No mcp-ticketer entry found in {codex_config_path}[/dim]")
190
+ return
191
+
192
+ # Step 5: Show what would be removed (dry run or actual removal)
193
+ if dry_run:
194
+ console.print("\n[cyan]DRY RUN - Would remove:[/cyan]")
195
+ console.print(" Server name: mcp-ticketer")
196
+ console.print(f" From: {codex_config_path}")
197
+ console.print(" Scope: Global (all sessions)")
198
+ return
199
+
200
+ # Step 6: Remove mcp-ticketer from configuration
201
+ del codex_config["mcp_servers"]["mcp-ticketer"]
202
+
203
+ # Step 7: Save updated configuration
204
+ try:
205
+ save_codex_config(codex_config_path, codex_config)
206
+ console.print("\n[green]✓ Successfully removed mcp-ticketer[/green]")
207
+ console.print(f"[dim]Configuration updated: {codex_config_path}[/dim]")
208
+
209
+ # Next steps
210
+ console.print("\n[bold cyan]Next Steps:[/bold cyan]")
211
+ console.print("1. [bold]Restart Codex CLI[/bold] (required for changes)")
212
+ console.print("2. mcp-ticketer will no longer be available via MCP")
213
+ console.print(
214
+ "\n[yellow]⚠ Note: This removes global configuration affecting all Codex sessions[/yellow]"
215
+ )
216
+
217
+ except Exception as e:
218
+ console.print(f"\n[red]✗ Failed to update configuration:[/red] {e}")
219
+ raise
220
+
221
+
154
222
  def configure_codex_mcp(force: bool = False) -> None:
155
223
  """Configure Codex CLI to use mcp-ticketer.
156
224
 
@@ -1,7 +1,6 @@
1
1
  """Interactive configuration wizard for MCP Ticketer."""
2
2
 
3
3
  import os
4
- from typing import Optional
5
4
 
6
5
  import typer
7
6
  from rich.console import Console
@@ -9,15 +8,9 @@ from rich.panel import Panel
9
8
  from rich.prompt import Confirm, Prompt
10
9
  from rich.table import Table
11
10
 
12
- from ..core.project_config import (
13
- AdapterConfig,
14
- AdapterType,
15
- ConfigResolver,
16
- ConfigValidator,
17
- HybridConfig,
18
- SyncStrategy,
19
- TicketerConfig,
20
- )
11
+ from ..core.project_config import (AdapterConfig, AdapterType, ConfigResolver,
12
+ ConfigValidator, HybridConfig, SyncStrategy,
13
+ TicketerConfig)
21
14
 
22
15
  console = Console()
23
16
 
@@ -440,10 +433,10 @@ def show_current_config() -> None:
440
433
 
441
434
 
442
435
  def set_adapter_config(
443
- adapter: Optional[str] = None,
444
- api_key: Optional[str] = None,
445
- project_id: Optional[str] = None,
446
- team_id: Optional[str] = None,
436
+ adapter: str | None = None,
437
+ api_key: str | None = None,
438
+ project_id: str | None = None,
439
+ team_id: str | None = None,
447
440
  global_scope: bool = False,
448
441
  **kwargs,
449
442
  ) -> None:
@@ -5,7 +5,7 @@ import logging
5
5
  import sys
6
6
  from datetime import datetime, timedelta
7
7
  from pathlib import Path
8
- from typing import Any, Optional
8
+ from typing import Any
9
9
 
10
10
  import typer
11
11
  from rich.console import Console
@@ -795,7 +795,7 @@ class SystemDiagnostics:
795
795
 
796
796
 
797
797
  async def run_diagnostics(
798
- output_file: Optional[str] = None,
798
+ output_file: str | None = None,
799
799
  json_output: bool = False,
800
800
  ) -> None:
801
801
  """Run comprehensive system diagnostics."""
@@ -1,18 +1,13 @@
1
1
  """CLI command for auto-discovering configuration from .env files."""
2
2
 
3
3
  from pathlib import Path
4
- from typing import Optional
5
4
 
6
5
  import typer
7
6
  from rich.console import Console
8
7
 
9
8
  from ..core.env_discovery import DiscoveredAdapter, EnvDiscovery
10
- from ..core.project_config import (
11
- AdapterConfig,
12
- ConfigResolver,
13
- ConfigValidator,
14
- TicketerConfig,
15
- )
9
+ from ..core.project_config import (AdapterConfig, ConfigResolver,
10
+ ConfigValidator, TicketerConfig)
16
11
 
17
12
  console = Console()
18
13
  app = typer.Typer(help="Auto-discover configuration from .env files")
@@ -93,7 +88,7 @@ def _display_discovered_adapter(
93
88
 
94
89
  @app.command()
95
90
  def show(
96
- project_path: Optional[Path] = typer.Option(
91
+ project_path: Path | None = typer.Option(
97
92
  None,
98
93
  "--path",
99
94
  "-p",
@@ -148,7 +143,7 @@ def show(
148
143
 
149
144
  @app.command()
150
145
  def save(
151
- adapter: Optional[str] = typer.Option(
146
+ adapter: str | None = typer.Option(
152
147
  None, "--adapter", "-a", help="Which adapter to save (defaults to recommended)"
153
148
  ),
154
149
  global_config: bool = typer.Option(
@@ -157,7 +152,7 @@ def save(
157
152
  dry_run: bool = typer.Option(
158
153
  False, "--dry-run", help="Show what would be saved without saving"
159
154
  ),
160
- project_path: Optional[Path] = typer.Option(
155
+ project_path: Path | None = typer.Option(
161
156
  None,
162
157
  "--path",
163
158
  "-p",
@@ -261,7 +256,7 @@ def save(
261
256
 
262
257
  @app.command()
263
258
  def interactive(
264
- project_path: Optional[Path] = typer.Option(
259
+ project_path: Path | None = typer.Option(
265
260
  None,
266
261
  "--path",
267
262
  "-p",
@@ -2,7 +2,7 @@
2
2
 
3
3
  import json
4
4
  from pathlib import Path
5
- from typing import Literal, Optional
5
+ from typing import Literal
6
6
 
7
7
  from rich.console import Console
8
8
 
@@ -73,7 +73,7 @@ def save_gemini_config(config_path: Path, config: dict) -> None:
73
73
 
74
74
 
75
75
  def create_gemini_server_config(
76
- binary_path: str, project_config: dict, cwd: Optional[str] = None
76
+ binary_path: str, project_config: dict, cwd: str | None = None
77
77
  ) -> dict:
78
78
  """Create Gemini MCP server configuration for mcp-ticketer.
79
79
 
@@ -147,6 +147,72 @@ def create_gemini_server_config(
147
147
  return config
148
148
 
149
149
 
150
+ def remove_gemini_mcp(
151
+ scope: Literal["project", "user"] = "project", dry_run: bool = False
152
+ ) -> None:
153
+ """Remove mcp-ticketer from Gemini CLI configuration.
154
+
155
+ Args:
156
+ scope: Configuration scope - "project" or "user"
157
+ dry_run: Show what would be removed without making changes
158
+
159
+ """
160
+ # Step 1: Find Gemini config location
161
+ config_type = "user-level" if scope == "user" else "project-level"
162
+ console.print(f"[cyan]🔍 Removing {config_type} Gemini CLI configuration...[/cyan]")
163
+
164
+ gemini_config_path = find_gemini_config(scope)
165
+ console.print(f"[dim]Config location: {gemini_config_path}[/dim]")
166
+
167
+ # Step 2: Check if config file exists
168
+ if not gemini_config_path.exists():
169
+ console.print(
170
+ f"[yellow]⚠ No configuration found at {gemini_config_path}[/yellow]"
171
+ )
172
+ console.print("[dim]mcp-ticketer is not configured for Gemini CLI[/dim]")
173
+ return
174
+
175
+ # Step 3: Load existing Gemini configuration
176
+ gemini_config = load_gemini_config(gemini_config_path)
177
+
178
+ # Step 4: Check if mcp-ticketer is configured
179
+ if "mcp-ticketer" not in gemini_config.get("mcpServers", {}):
180
+ console.print("[yellow]⚠ mcp-ticketer is not configured[/yellow]")
181
+ console.print(f"[dim]No mcp-ticketer entry found in {gemini_config_path}[/dim]")
182
+ return
183
+
184
+ # Step 5: Show what would be removed (dry run or actual removal)
185
+ if dry_run:
186
+ console.print("\n[cyan]DRY RUN - Would remove:[/cyan]")
187
+ console.print(" Server name: mcp-ticketer")
188
+ console.print(f" From: {gemini_config_path}")
189
+ console.print(f" Scope: {config_type}")
190
+ return
191
+
192
+ # Step 6: Remove mcp-ticketer from configuration
193
+ del gemini_config["mcpServers"]["mcp-ticketer"]
194
+
195
+ # Step 7: Save updated configuration
196
+ try:
197
+ save_gemini_config(gemini_config_path, gemini_config)
198
+ console.print("\n[green]✓ Successfully removed mcp-ticketer[/green]")
199
+ console.print(f"[dim]Configuration updated: {gemini_config_path}[/dim]")
200
+
201
+ # Next steps
202
+ console.print("\n[bold cyan]Next Steps:[/bold cyan]")
203
+ if scope == "user":
204
+ console.print("1. Gemini CLI global configuration updated")
205
+ console.print("2. mcp-ticketer will no longer be available in any project")
206
+ else:
207
+ console.print("1. Gemini CLI project configuration updated")
208
+ console.print("2. mcp-ticketer will no longer be available in this project")
209
+ console.print("3. Restart Gemini CLI if currently running")
210
+
211
+ except Exception as e:
212
+ console.print(f"\n[red]✗ Failed to update configuration:[/red] {e}")
213
+ raise
214
+
215
+
150
216
  def configure_gemini_mcp(
151
217
  scope: Literal["project", "user"] = "project", force: bool = False
152
218
  ) -> None: