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.
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/cli/auggie_configure.py +66 -0
- mcp_ticketer/cli/codex_configure.py +68 -0
- mcp_ticketer/cli/gemini_configure.py +66 -0
- mcp_ticketer/cli/main.py +276 -39
- mcp_ticketer/cli/mcp_configure.py +71 -8
- mcp_ticketer/cli/platform_commands.py +5 -15
- mcp_ticketer/cli/ticket_commands.py +15 -5
- mcp_ticketer/mcp/server_sdk.py +93 -0
- mcp_ticketer/mcp/tools/__init__.py +38 -0
- mcp_ticketer/mcp/tools/attachment_tools.py +180 -0
- mcp_ticketer/mcp/tools/bulk_tools.py +273 -0
- mcp_ticketer/mcp/tools/comment_tools.py +90 -0
- mcp_ticketer/mcp/tools/hierarchy_tools.py +383 -0
- mcp_ticketer/mcp/tools/pr_tools.py +154 -0
- mcp_ticketer/mcp/tools/search_tools.py +206 -0
- mcp_ticketer/mcp/tools/ticket_tools.py +277 -0
- {mcp_ticketer-0.4.0.dist-info → mcp_ticketer-0.4.2.dist-info}/METADATA +30 -16
- {mcp_ticketer-0.4.0.dist-info → mcp_ticketer-0.4.2.dist-info}/RECORD +23 -14
- {mcp_ticketer-0.4.0.dist-info → mcp_ticketer-0.4.2.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.4.0.dist-info → mcp_ticketer-0.4.2.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.4.0.dist-info → mcp_ticketer-0.4.2.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.4.0.dist-info → mcp_ticketer-0.4.2.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
+
}
|