mcp-ticketer 0.4.0__py3-none-any.whl → 0.4.2__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.

@@ -16,13 +16,21 @@ def find_mcp_ticketer_binary() -> str:
16
16
  """Find the mcp-ticketer binary path.
17
17
 
18
18
  Returns:
19
- Path to mcp-ticketer binary
19
+ Path to mcp-ticketer binary (prefers simple 'mcp-ticketer' if in PATH)
20
20
 
21
21
  Raises:
22
22
  FileNotFoundError: If binary not found
23
23
 
24
24
  """
25
- # Check if running from development environment
25
+ # PRIORITY 1: Check PATH first (like kuzu-memory)
26
+ # This allows the system to resolve the binary location
27
+ which_result = shutil.which("mcp-ticketer")
28
+ if which_result:
29
+ # Return just "mcp-ticketer" for PATH-based installations
30
+ # This is more portable and matches kuzu-memory's approach
31
+ return "mcp-ticketer"
32
+
33
+ # FALLBACK: Check development environment
26
34
  import mcp_ticketer
27
35
 
28
36
  package_path = Path(mcp_ticketer.__file__).parent.parent.parent
@@ -45,11 +53,6 @@ def find_mcp_ticketer_binary() -> str:
45
53
  / "mcp-ticketer",
46
54
  ]
47
55
 
48
- # Check PATH
49
- which_result = shutil.which("mcp-ticketer")
50
- if which_result:
51
- return which_result
52
-
53
56
  # Check possible paths
54
57
  for path in possible_paths:
55
58
  if path.exists():
@@ -183,7 +186,7 @@ def create_mcp_server_config(
183
186
  """
184
187
  config = {
185
188
  "command": binary_path,
186
- "args": ["serve"], # Use 'serve' command to start MCP server
189
+ "args": ["mcp", "serve"], # Use 'mcp serve' command to start MCP server
187
190
  }
188
191
 
189
192
  # Add working directory if provided
@@ -214,6 +217,66 @@ def create_mcp_server_config(
214
217
  return config
215
218
 
216
219
 
220
+ def remove_claude_mcp(global_config: bool = False, dry_run: bool = False) -> None:
221
+ """Remove mcp-ticketer from Claude Code/Desktop configuration.
222
+
223
+ Args:
224
+ global_config: Remove from Claude Desktop instead of project-level
225
+ dry_run: Show what would be removed without making changes
226
+
227
+ """
228
+ # Step 1: Find Claude MCP config location
229
+ config_type = "Claude Desktop" if global_config else "project-level"
230
+ console.print(f"[cyan]🔍 Removing {config_type} MCP configuration...[/cyan]")
231
+
232
+ mcp_config_path = find_claude_mcp_config(global_config)
233
+ console.print(f"[dim]Config location: {mcp_config_path}[/dim]")
234
+
235
+ # Step 2: Check if config file exists
236
+ if not mcp_config_path.exists():
237
+ console.print(f"[yellow]⚠ No configuration found at {mcp_config_path}[/yellow]")
238
+ console.print("[dim]mcp-ticketer is not configured for this platform[/dim]")
239
+ return
240
+
241
+ # Step 3: Load existing MCP configuration
242
+ mcp_config = load_claude_mcp_config(mcp_config_path)
243
+
244
+ # Step 4: Check if mcp-ticketer is configured
245
+ if "mcp-ticketer" not in mcp_config.get("mcpServers", {}):
246
+ console.print("[yellow]⚠ mcp-ticketer is not configured[/yellow]")
247
+ console.print(f"[dim]No mcp-ticketer entry found in {mcp_config_path}[/dim]")
248
+ return
249
+
250
+ # Step 5: Show what would be removed (dry run or actual removal)
251
+ if dry_run:
252
+ console.print("\n[cyan]DRY RUN - Would remove:[/cyan]")
253
+ console.print(" Server name: mcp-ticketer")
254
+ console.print(f" From: {mcp_config_path}")
255
+ return
256
+
257
+ # Step 6: Remove mcp-ticketer from configuration
258
+ del mcp_config["mcpServers"]["mcp-ticketer"]
259
+
260
+ # Step 7: Save updated configuration
261
+ try:
262
+ save_claude_mcp_config(mcp_config_path, mcp_config)
263
+ console.print("\n[green]✓ Successfully removed mcp-ticketer[/green]")
264
+ console.print(f"[dim]Configuration updated: {mcp_config_path}[/dim]")
265
+
266
+ # Next steps
267
+ console.print("\n[bold cyan]Next Steps:[/bold cyan]")
268
+ if global_config:
269
+ console.print("1. Restart Claude Desktop")
270
+ console.print("2. mcp-ticketer will no longer be available in MCP menu")
271
+ else:
272
+ console.print("1. Restart Claude Code")
273
+ console.print("2. mcp-ticketer will no longer be available in this project")
274
+
275
+ except Exception as e:
276
+ console.print(f"\n[red]✗ Failed to update configuration:[/red] {e}")
277
+ raise
278
+
279
+
217
280
  def configure_claude_mcp(global_config: bool = False, force: bool = False) -> None:
218
281
  """Configure Claude Code to use mcp-ticketer.
219
282
 
@@ -29,9 +29,7 @@ def jira_list_projects():
29
29
  from rich.console import Console
30
30
 
31
31
  console = Console()
32
- console.print(
33
- "[yellow]JIRA platform commands are not yet implemented.[/yellow]"
34
- )
32
+ console.print("[yellow]JIRA platform commands are not yet implemented.[/yellow]")
35
33
  console.print(
36
34
  "Use the generic ticket commands for JIRA operations:\n"
37
35
  " mcp-ticketer ticket create 'My ticket'\n"
@@ -45,12 +43,8 @@ def jira_configure():
45
43
  from rich.console import Console
46
44
 
47
45
  console = Console()
48
- console.print(
49
- "[yellow]JIRA platform commands are not yet implemented.[/yellow]"
50
- )
51
- console.print(
52
- "Use 'mcp-ticketer init --adapter jira' to configure JIRA adapter."
53
- )
46
+ console.print("[yellow]JIRA platform commands are not yet implemented.[/yellow]")
47
+ console.print("Use 'mcp-ticketer init --adapter jira' to configure JIRA adapter.")
54
48
 
55
49
 
56
50
  # GitHub platform commands (placeholder)
@@ -66,9 +60,7 @@ def github_list_repos():
66
60
  from rich.console import Console
67
61
 
68
62
  console = Console()
69
- console.print(
70
- "[yellow]GitHub platform commands are not yet implemented.[/yellow]"
71
- )
63
+ console.print("[yellow]GitHub platform commands are not yet implemented.[/yellow]")
72
64
  console.print(
73
65
  "Use the generic ticket commands for GitHub operations:\n"
74
66
  " mcp-ticketer ticket create 'My issue'\n"
@@ -82,9 +74,7 @@ def github_configure():
82
74
  from rich.console import Console
83
75
 
84
76
  console = Console()
85
- console.print(
86
- "[yellow]GitHub platform commands are not yet implemented.[/yellow]"
87
- )
77
+ console.print("[yellow]GitHub platform commands are not yet implemented.[/yellow]")
88
78
  console.print(
89
79
  "Use 'mcp-ticketer init --adapter github' to configure GitHub adapter."
90
80
  )
@@ -52,7 +52,9 @@ def load_config(project_dir: Optional[Path] = None) -> dict:
52
52
  return config
53
53
  except (OSError, json.JSONDecodeError) as e:
54
54
  logger.warning(f"Could not load project config: {e}, using defaults")
55
- console.print(f"[yellow]Warning: Could not load project config: {e}[/yellow]")
55
+ console.print(
56
+ f"[yellow]Warning: Could not load project config: {e}[/yellow]"
57
+ )
56
58
 
57
59
  logger.info("No project-local config found, defaulting to aitrackdown adapter")
58
60
  return {"adapter": "aitrackdown", "config": {"base_path": ".aitrackdown"}}
@@ -70,7 +72,9 @@ def save_config(config: dict) -> None:
70
72
  logger.info(f"Saved configuration to: {project_config}")
71
73
 
72
74
 
73
- def get_adapter(override_adapter: Optional[str] = None, override_config: Optional[dict] = None):
75
+ def get_adapter(
76
+ override_adapter: Optional[str] = None, override_config: Optional[dict] = None
77
+ ):
74
78
  """Get configured adapter instance."""
75
79
  config = load_config()
76
80
 
@@ -369,7 +373,9 @@ def create(
369
373
  console.print(f" Title: {title}")
370
374
  console.print(f" Priority: {priority}")
371
375
  console.print(f" Adapter: {adapter_name}")
372
- console.print("[dim]Use 'mcp-ticketer ticket check {queue_id}' to check progress[/dim]")
376
+ console.print(
377
+ "[dim]Use 'mcp-ticketer ticket check {queue_id}' to check progress[/dim]"
378
+ )
373
379
 
374
380
  # Start worker if needed with immediate feedback
375
381
  manager = WorkerManager()
@@ -606,7 +612,9 @@ def update(
606
612
  for key, value in updates.items():
607
613
  if key != "ticket_id":
608
614
  console.print(f" {key}: {value}")
609
- console.print("[dim]Use 'mcp-ticketer ticket check {queue_id}' to check progress[/dim]")
615
+ console.print(
616
+ "[dim]Use 'mcp-ticketer ticket check {queue_id}' to check progress[/dim]"
617
+ )
610
618
 
611
619
  # Start worker if needed
612
620
  manager = WorkerManager()
@@ -672,7 +680,9 @@ def transition(
672
680
 
673
681
  console.print(f"[green]✓[/green] Queued state transition: {queue_id}")
674
682
  console.print(f" Ticket: {ticket_id} → {target_state}")
675
- console.print("[dim]Use 'mcp-ticketer ticket check {queue_id}' to check progress[/dim]")
683
+ console.print(
684
+ "[dim]Use 'mcp-ticketer ticket check {queue_id}' to check progress[/dim]"
685
+ )
676
686
 
677
687
  # Start worker if needed
678
688
  manager = WorkerManager()
@@ -0,0 +1,93 @@
1
+ """FastMCP-based MCP server implementation.
2
+
3
+ This module implements the MCP server using the official FastMCP SDK,
4
+ replacing the custom JSON-RPC implementation. It provides a cleaner,
5
+ more maintainable approach with automatic schema generation and
6
+ better error handling.
7
+
8
+ The server manages a global adapter instance that is configured at
9
+ startup and used by all tool implementations.
10
+ """
11
+
12
+ import logging
13
+ from typing import Any, Optional
14
+
15
+ from mcp.server.fastmcp import FastMCP
16
+
17
+ from ..core.adapter import BaseAdapter
18
+ from ..core.registry import AdapterRegistry
19
+
20
+ # Initialize FastMCP server
21
+ mcp = FastMCP("mcp-ticketer")
22
+
23
+ # Global adapter instance
24
+ _adapter: Optional[BaseAdapter] = None
25
+
26
+ # Configure logging
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ def configure_adapter(adapter_type: str, config: dict[str, Any]) -> None:
31
+ """Configure the global adapter instance.
32
+
33
+ This must be called before starting the server to initialize the
34
+ adapter that will handle all ticket operations.
35
+
36
+ Args:
37
+ adapter_type: Type of adapter to create (e.g., "linear", "jira", "github")
38
+ config: Configuration dictionary for the adapter
39
+
40
+ Raises:
41
+ ValueError: If adapter type is not registered
42
+ RuntimeError: If adapter configuration fails
43
+
44
+ """
45
+ global _adapter
46
+
47
+ try:
48
+ # Get adapter from registry
49
+ _adapter = AdapterRegistry.get_adapter(adapter_type, config)
50
+ logger.info(f"Configured {adapter_type} adapter for MCP server")
51
+ except Exception as e:
52
+ logger.error(f"Failed to configure adapter: {e}")
53
+ raise RuntimeError(f"Adapter configuration failed: {e}") from e
54
+
55
+
56
+ def get_adapter() -> BaseAdapter:
57
+ """Get the configured adapter instance.
58
+
59
+ Returns:
60
+ The global adapter instance
61
+
62
+ Raises:
63
+ RuntimeError: If adapter has not been configured
64
+
65
+ """
66
+ if _adapter is None:
67
+ raise RuntimeError(
68
+ "Adapter not configured. Call configure_adapter() before starting server."
69
+ )
70
+ return _adapter
71
+
72
+
73
+ # Import all tool modules to register them with FastMCP
74
+ # These imports must come after mcp is initialized but before main()
75
+ from . import tools # noqa: E402, F401
76
+
77
+
78
+ def main() -> None:
79
+ """Run the FastMCP server.
80
+
81
+ This function starts the server using stdio transport for
82
+ JSON-RPC communication with Claude Desktop/Code.
83
+
84
+ The adapter must be configured via configure_adapter() before
85
+ calling this function.
86
+
87
+ """
88
+ # Run the server with stdio transport
89
+ mcp.run(transport="stdio")
90
+
91
+
92
+ if __name__ == "__main__":
93
+ main()
@@ -0,0 +1,38 @@
1
+ """MCP tool modules for ticket operations.
2
+
3
+ This package contains all FastMCP tool implementations organized by
4
+ functional area. Tools are automatically registered with the FastMCP
5
+ server when imported.
6
+
7
+ Modules:
8
+ ticket_tools: Basic CRUD operations for tickets
9
+ hierarchy_tools: Epic/Issue/Task hierarchy management
10
+ search_tools: Search and query operations
11
+ bulk_tools: Bulk create and update operations
12
+ comment_tools: Comment management
13
+ pr_tools: Pull request integration
14
+ attachment_tools: File attachment handling
15
+
16
+ """
17
+
18
+ # Import all tool modules to register them with FastMCP
19
+ # Order matters - import core functionality first
20
+ from . import (
21
+ attachment_tools, # noqa: F401
22
+ bulk_tools, # noqa: F401
23
+ comment_tools, # noqa: F401
24
+ hierarchy_tools, # noqa: F401
25
+ pr_tools, # noqa: F401
26
+ search_tools, # noqa: F401
27
+ ticket_tools, # noqa: F401
28
+ )
29
+
30
+ __all__ = [
31
+ "ticket_tools",
32
+ "hierarchy_tools",
33
+ "search_tools",
34
+ "bulk_tools",
35
+ "comment_tools",
36
+ "pr_tools",
37
+ "attachment_tools",
38
+ ]
@@ -0,0 +1,180 @@
1
+ """Attachment management tools for tickets.
2
+
3
+ This module implements tools for attaching files to tickets and retrieving
4
+ attachment information. Note that file attachment functionality may not be
5
+ available in all adapters.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ from ..server_sdk import get_adapter, mcp
11
+
12
+
13
+ @mcp.tool()
14
+ async def ticket_attach(
15
+ ticket_id: str,
16
+ file_path: str,
17
+ description: str = "",
18
+ ) -> dict[str, Any]:
19
+ """Attach a file to a ticket.
20
+
21
+ Uploads a file and associates it with the specified ticket. This
22
+ functionality may not be available in all adapters.
23
+
24
+ Args:
25
+ ticket_id: Unique identifier of the ticket
26
+ file_path: Path to the file to attach
27
+ description: Optional description of the attachment
28
+
29
+ Returns:
30
+ Attachment details including URL or ID, or error information
31
+
32
+ """
33
+ try:
34
+ adapter = get_adapter()
35
+
36
+ # Read ticket to validate it exists
37
+ ticket = await adapter.read(ticket_id)
38
+ if ticket is None:
39
+ return {
40
+ "status": "error",
41
+ "error": f"Ticket {ticket_id} not found",
42
+ }
43
+
44
+ # Check if adapter supports attachments
45
+ if not hasattr(adapter, "add_attachment"):
46
+ return {
47
+ "status": "error",
48
+ "error": f"File attachments not supported by {type(adapter).__name__} adapter",
49
+ "ticket_id": ticket_id,
50
+ "note": "Consider using ticket_comment to add a reference to the file location",
51
+ }
52
+
53
+ # Add attachment via adapter
54
+ attachment = await adapter.add_attachment( # type: ignore
55
+ ticket_id=ticket_id, file_path=file_path, description=description
56
+ )
57
+
58
+ return {
59
+ "status": "completed",
60
+ "ticket_id": ticket_id,
61
+ "attachment": attachment,
62
+ }
63
+
64
+ except AttributeError:
65
+ # Fallback: Add file reference as comment
66
+ from ...core.models import Comment
67
+
68
+ comment_text = f"Attachment: {file_path}"
69
+ if description:
70
+ comment_text += f"\nDescription: {description}"
71
+
72
+ comment = Comment(
73
+ ticket_id=ticket_id,
74
+ content=comment_text,
75
+ )
76
+
77
+ created_comment = await adapter.add_comment(comment)
78
+
79
+ return {
80
+ "status": "completed",
81
+ "ticket_id": ticket_id,
82
+ "method": "comment_reference",
83
+ "file_path": file_path,
84
+ "comment": created_comment.model_dump(),
85
+ "note": "Adapter does not support direct file uploads. File reference added as comment.",
86
+ }
87
+
88
+ except FileNotFoundError:
89
+ return {
90
+ "status": "error",
91
+ "error": f"File not found: {file_path}",
92
+ "ticket_id": ticket_id,
93
+ }
94
+ except Exception as e:
95
+ return {
96
+ "status": "error",
97
+ "error": f"Failed to attach file: {str(e)}",
98
+ "ticket_id": ticket_id,
99
+ }
100
+
101
+
102
+ @mcp.tool()
103
+ async def ticket_attachments(
104
+ ticket_id: str,
105
+ ) -> dict[str, Any]:
106
+ """Get all attachments for a ticket.
107
+
108
+ Retrieves a list of all files attached to the specified ticket.
109
+ This functionality may not be available in all adapters.
110
+
111
+ Args:
112
+ ticket_id: Unique identifier of the ticket
113
+
114
+ Returns:
115
+ List of attachments with metadata, or error information
116
+
117
+ """
118
+ try:
119
+ adapter = get_adapter()
120
+
121
+ # Read ticket to validate it exists
122
+ ticket = await adapter.read(ticket_id)
123
+ if ticket is None:
124
+ return {
125
+ "status": "error",
126
+ "error": f"Ticket {ticket_id} not found",
127
+ }
128
+
129
+ # Check if adapter supports attachments
130
+ if not hasattr(adapter, "get_attachments"):
131
+ return {
132
+ "status": "error",
133
+ "error": f"Attachment retrieval not supported by {type(adapter).__name__} adapter",
134
+ "ticket_id": ticket_id,
135
+ "note": "Check ticket comments for file references",
136
+ }
137
+
138
+ # Get attachments via adapter
139
+ attachments = await adapter.get_attachments(ticket_id) # type: ignore
140
+
141
+ return {
142
+ "status": "completed",
143
+ "ticket_id": ticket_id,
144
+ "attachments": attachments,
145
+ "count": len(attachments) if isinstance(attachments, list) else 0,
146
+ }
147
+
148
+ except AttributeError:
149
+ # Fallback: Check comments for attachment references
150
+ comments = await adapter.get_comments(ticket_id=ticket_id, limit=100)
151
+
152
+ # Look for comments that reference files
153
+ attachment_refs = []
154
+ for comment in comments:
155
+ content = comment.content or ""
156
+ if content.startswith("Attachment:") or "file://" in content:
157
+ attachment_refs.append(
158
+ {
159
+ "type": "comment_reference",
160
+ "comment_id": comment.id,
161
+ "content": content,
162
+ "created_at": comment.created_at,
163
+ }
164
+ )
165
+
166
+ return {
167
+ "status": "completed",
168
+ "ticket_id": ticket_id,
169
+ "method": "comment_references",
170
+ "attachments": attachment_refs,
171
+ "count": len(attachment_refs),
172
+ "note": "Adapter does not support direct attachments. Showing file references from comments.",
173
+ }
174
+
175
+ except Exception as e:
176
+ return {
177
+ "status": "error",
178
+ "error": f"Failed to get attachments: {str(e)}",
179
+ "ticket_id": ticket_id,
180
+ }