mcp-ticketer 0.4.4__py3-none-any.whl → 0.4.8__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 (45) hide show
  1. mcp_ticketer/__init__.py +3 -12
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/adapters/aitrackdown.py +2 -9
  4. mcp_ticketer/adapters/github.py +2 -1
  5. mcp_ticketer/adapters/jira.py +2 -1
  6. mcp_ticketer/adapters/linear/adapter.py +28 -25
  7. mcp_ticketer/adapters/linear/client.py +2 -1
  8. mcp_ticketer/adapters/linear/mappers.py +2 -1
  9. mcp_ticketer/cli/adapter_diagnostics.py +4 -2
  10. mcp_ticketer/cli/auggie_configure.py +35 -15
  11. mcp_ticketer/cli/codex_configure.py +38 -31
  12. mcp_ticketer/cli/configure.py +3 -9
  13. mcp_ticketer/cli/discover.py +2 -6
  14. mcp_ticketer/cli/gemini_configure.py +38 -25
  15. mcp_ticketer/cli/main.py +4 -3
  16. mcp_ticketer/cli/mcp_configure.py +115 -78
  17. mcp_ticketer/cli/python_detection.py +126 -0
  18. mcp_ticketer/core/__init__.py +2 -1
  19. mcp_ticketer/mcp/__init__.py +29 -1
  20. mcp_ticketer/mcp/__main__.py +60 -0
  21. mcp_ticketer/mcp/server/__init__.py +21 -0
  22. mcp_ticketer/mcp/server/__main__.py +60 -0
  23. mcp_ticketer/mcp/{server.py → server/main.py} +15 -35
  24. mcp_ticketer/mcp/{tools → server/tools}/__init__.py +7 -9
  25. mcp_ticketer/queue/__init__.py +1 -0
  26. mcp_ticketer/queue/manager.py +10 -46
  27. mcp_ticketer/queue/ticket_registry.py +5 -5
  28. {mcp_ticketer-0.4.4.dist-info → mcp_ticketer-0.4.8.dist-info}/METADATA +13 -4
  29. mcp_ticketer-0.4.8.dist-info/RECORD +77 -0
  30. mcp_ticketer-0.4.4.dist-info/RECORD +0 -73
  31. /mcp_ticketer/mcp/{constants.py → server/constants.py} +0 -0
  32. /mcp_ticketer/mcp/{dto.py → server/dto.py} +0 -0
  33. /mcp_ticketer/mcp/{response_builder.py → server/response_builder.py} +0 -0
  34. /mcp_ticketer/mcp/{server_sdk.py → server/server_sdk.py} +0 -0
  35. /mcp_ticketer/mcp/{tools → server/tools}/attachment_tools.py +0 -0
  36. /mcp_ticketer/mcp/{tools → server/tools}/bulk_tools.py +0 -0
  37. /mcp_ticketer/mcp/{tools → server/tools}/comment_tools.py +0 -0
  38. /mcp_ticketer/mcp/{tools → server/tools}/hierarchy_tools.py +0 -0
  39. /mcp_ticketer/mcp/{tools → server/tools}/pr_tools.py +0 -0
  40. /mcp_ticketer/mcp/{tools → server/tools}/search_tools.py +0 -0
  41. /mcp_ticketer/mcp/{tools → server/tools}/ticket_tools.py +0 -0
  42. {mcp_ticketer-0.4.4.dist-info → mcp_ticketer-0.4.8.dist-info}/WHEEL +0 -0
  43. {mcp_ticketer-0.4.4.dist-info → mcp_ticketer-0.4.8.dist-info}/entry_points.txt +0 -0
  44. {mcp_ticketer-0.4.4.dist-info → mcp_ticketer-0.4.8.dist-info}/licenses/LICENSE +0 -0
  45. {mcp_ticketer-0.4.4.dist-info → mcp_ticketer-0.4.8.dist-info}/top_level.txt +0 -0
@@ -2,65 +2,43 @@
2
2
 
3
3
  import json
4
4
  import os
5
- import shutil
6
5
  import sys
7
6
  from pathlib import Path
8
7
 
9
8
  from rich.console import Console
10
9
 
10
+ from .python_detection import get_mcp_ticketer_python
11
+
11
12
  console = Console()
12
13
 
13
14
 
14
- def find_mcp_ticketer_binary() -> str:
15
- """Find the mcp-ticketer binary path.
15
+ def load_env_file(env_path: Path) -> dict[str, str]:
16
+ """Load environment variables from .env file.
16
17
 
17
- Returns:
18
- Path to mcp-ticketer binary (prefers simple 'mcp-ticketer' if in PATH)
18
+ Args:
19
+ env_path: Path to .env file
19
20
 
20
- Raises:
21
- FileNotFoundError: If binary not found
21
+ Returns:
22
+ Dict of environment variable key-value pairs
22
23
 
23
24
  """
24
- # PRIORITY 1: Check PATH first (like kuzu-memory)
25
- # This allows the system to resolve the binary location
26
- which_result = shutil.which("mcp-ticketer")
27
- if which_result:
28
- # Return just "mcp-ticketer" for PATH-based installations
29
- # This is more portable and matches kuzu-memory's approach
30
- return "mcp-ticketer"
31
-
32
- # FALLBACK: Check development environment
33
- import mcp_ticketer
34
-
35
- package_path = Path(mcp_ticketer.__file__).parent.parent.parent
36
-
37
- # Check for virtual environment bin
38
- possible_paths = [
39
- # Development paths
40
- package_path / "venv" / "bin" / "mcp-ticketer",
41
- package_path / ".venv" / "bin" / "mcp-ticketer",
42
- package_path / "test_venv" / "bin" / "mcp-ticketer",
43
- # System installation
44
- Path.home() / ".local" / "bin" / "mcp-ticketer",
45
- # pipx installation
46
- Path.home()
47
- / ".local"
48
- / "pipx"
49
- / "venvs"
50
- / "mcp-ticketer"
51
- / "bin"
52
- / "mcp-ticketer",
53
- ]
54
-
55
- # Check possible paths
56
- for path in possible_paths:
57
- if path.exists():
58
- return str(path.resolve())
59
-
60
- raise FileNotFoundError(
61
- "Could not find mcp-ticketer binary. Please ensure mcp-ticketer is installed.\n"
62
- "Install with: pip install mcp-ticketer"
63
- )
25
+ env_vars = {}
26
+ if not env_path.exists():
27
+ return env_vars
28
+
29
+ with open(env_path) as f:
30
+ for line in f:
31
+ line = line.strip()
32
+ # Skip comments and empty lines
33
+ if not line or line.startswith("#"):
34
+ continue
35
+
36
+ # Parse KEY=VALUE format
37
+ if "=" in line:
38
+ key, value = line.split("=", 1)
39
+ env_vars[key.strip()] = value.strip()
40
+
41
+ return env_vars
64
42
 
65
43
 
66
44
  def load_project_config() -> dict:
@@ -129,8 +107,8 @@ def find_claude_mcp_config(global_config: bool = False) -> Path:
129
107
  Path.home() / ".config" / "Claude" / "claude_desktop_config.json"
130
108
  )
131
109
  else:
132
- # Project-level configuration
133
- config_path = Path.cwd() / ".mcp" / "config.json"
110
+ # Project-level configuration for Claude Code
111
+ config_path = Path.cwd() / ".claude" / "settings.local.json"
134
112
 
135
113
  return config_path
136
114
 
@@ -149,7 +127,7 @@ def load_claude_mcp_config(config_path: Path) -> dict:
149
127
  with open(config_path) as f:
150
128
  return json.load(f)
151
129
 
152
- # Return empty structure
130
+ # Return empty structure (Claude Code uses mcpServers key)
153
131
  return {"mcpServers": {}}
154
132
 
155
133
 
@@ -170,28 +148,37 @@ def save_claude_mcp_config(config_path: Path, config: dict) -> None:
170
148
 
171
149
 
172
150
  def create_mcp_server_config(
173
- binary_path: str, project_config: dict, cwd: str | None = None
151
+ python_path: str, project_config: dict, project_path: str | None = None
174
152
  ) -> dict:
175
153
  """Create MCP server configuration for mcp-ticketer.
176
154
 
177
155
  Args:
178
- binary_path: Path to mcp-ticketer binary
156
+ python_path: Path to Python executable in mcp-ticketer venv
179
157
  project_config: Project configuration from .mcp-ticketer/config.json
180
- cwd: Working directory for server (optional)
158
+ project_path: Project directory path (optional)
181
159
 
182
160
  Returns:
183
- MCP server configuration dict
161
+ MCP server configuration dict matching Claude Code stdio pattern
184
162
 
185
163
  """
164
+ # Ensure python3 is used (not python)
165
+ if python_path.endswith("/python"):
166
+ python_path = python_path.replace("/python", "/python3")
167
+
168
+ # Use module invocation pattern: python -m mcp_ticketer.mcp.server
169
+ args = ["-m", "mcp_ticketer.mcp.server"]
170
+
171
+ # Add project path if provided
172
+ if project_path:
173
+ args.append(project_path)
174
+
175
+ # REQUIRED: Add "type": "stdio" for Claude Code compatibility
186
176
  config = {
187
- "command": binary_path,
188
- "args": ["mcp", "serve"], # Use 'mcp serve' command to start MCP server
177
+ "type": "stdio",
178
+ "command": python_path,
179
+ "args": args,
189
180
  }
190
181
 
191
- # Add working directory if provided
192
- if cwd:
193
- config["cwd"] = cwd
194
-
195
182
  # Add environment variables based on adapter
196
183
  adapter = project_config.get("default_adapter", "aitrackdown")
197
184
  adapters_config = project_config.get("adapters", {})
@@ -199,15 +186,49 @@ def create_mcp_server_config(
199
186
 
200
187
  env_vars = {}
201
188
 
202
- # Add adapter-specific environment variables
189
+ # Add PYTHONPATH for project context
190
+ if project_path:
191
+ env_vars["PYTHONPATH"] = project_path
192
+
193
+ # Add MCP_TICKETER_ADAPTER to identify which adapter to use
194
+ env_vars["MCP_TICKETER_ADAPTER"] = adapter
195
+
196
+ # Load environment variables from .env.local if it exists
197
+ if project_path:
198
+ env_file_path = Path(project_path) / ".env.local"
199
+ env_file_vars = load_env_file(env_file_path)
200
+
201
+ # Add relevant adapter-specific vars from .env.local
202
+ adapter_env_keys = {
203
+ "linear": ["LINEAR_API_KEY", "LINEAR_TEAM_ID", "LINEAR_TEAM_KEY"],
204
+ "github": ["GITHUB_TOKEN", "GITHUB_OWNER", "GITHUB_REPO"],
205
+ "jira": [
206
+ "JIRA_ACCESS_USER",
207
+ "JIRA_ACCESS_TOKEN",
208
+ "JIRA_ORGANIZATION_ID",
209
+ "JIRA_URL",
210
+ "JIRA_EMAIL",
211
+ "JIRA_API_TOKEN",
212
+ ],
213
+ "aitrackdown": [], # No specific env vars needed
214
+ }
215
+
216
+ # Include adapter-specific env vars from .env.local
217
+ for key in adapter_env_keys.get(adapter, []):
218
+ if key in env_file_vars:
219
+ env_vars[key] = env_file_vars[key]
220
+
221
+ # Fallback: Add adapter-specific environment variables from project config
203
222
  if adapter == "linear" and "api_key" in adapter_config:
204
- env_vars["LINEAR_API_KEY"] = adapter_config["api_key"]
223
+ if "LINEAR_API_KEY" not in env_vars:
224
+ env_vars["LINEAR_API_KEY"] = adapter_config["api_key"]
205
225
  elif adapter == "github" and "token" in adapter_config:
206
- env_vars["GITHUB_TOKEN"] = adapter_config["token"]
226
+ if "GITHUB_TOKEN" not in env_vars:
227
+ env_vars["GITHUB_TOKEN"] = adapter_config["token"]
207
228
  elif adapter == "jira":
208
- if "api_token" in adapter_config:
229
+ if "api_token" in adapter_config and "JIRA_API_TOKEN" not in env_vars:
209
230
  env_vars["JIRA_API_TOKEN"] = adapter_config["api_token"]
210
- if "email" in adapter_config:
231
+ if "email" in adapter_config and "JIRA_EMAIL" not in env_vars:
211
232
  env_vars["JIRA_EMAIL"] = adapter_config["email"]
212
233
 
213
234
  if env_vars:
@@ -284,18 +305,31 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
284
305
  force: Overwrite existing configuration
285
306
 
286
307
  Raises:
287
- FileNotFoundError: If binary or project config not found
308
+ FileNotFoundError: If Python executable or project config not found
288
309
  ValueError: If configuration is invalid
289
310
 
290
311
  """
291
- # Step 1: Find mcp-ticketer binary
292
- console.print("[cyan]🔍 Finding mcp-ticketer binary...[/cyan]")
312
+ # Determine project path for venv detection
313
+ project_path = Path.cwd() if not global_config else None
314
+
315
+ # Step 1: Find Python executable (project-specific if available)
316
+ console.print("[cyan]🔍 Finding mcp-ticketer Python executable...[/cyan]")
293
317
  try:
294
- binary_path = find_mcp_ticketer_binary()
295
- console.print(f"[green]✓[/green] Found: {binary_path}")
296
- except FileNotFoundError as e:
297
- console.print(f"[red]✗[/red] {e}")
298
- raise
318
+ python_path = get_mcp_ticketer_python(project_path=project_path)
319
+ console.print(f"[green]✓[/green] Found: {python_path}")
320
+
321
+ # Show if using project venv or fallback
322
+ if project_path and str(project_path / ".venv") in python_path:
323
+ console.print("[dim]Using project-specific venv[/dim]")
324
+ else:
325
+ console.print("[dim]Using pipx/system Python[/dim]")
326
+ except Exception as e:
327
+ console.print(f"[red]✗[/red] Could not find Python executable: {e}")
328
+ raise FileNotFoundError(
329
+ "Could not find mcp-ticketer Python executable. "
330
+ "Please ensure mcp-ticketer is installed.\n"
331
+ "Install with: pip install mcp-ticketer or pipx install mcp-ticketer"
332
+ )
299
333
 
300
334
  # Step 2: Load project configuration
301
335
  console.print("\n[cyan]📖 Reading project configuration...[/cyan]")
@@ -327,9 +361,11 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
327
361
  console.print("[yellow]⚠ Overwriting existing configuration[/yellow]")
328
362
 
329
363
  # Step 6: Create mcp-ticketer server config
330
- cwd = str(Path.cwd()) if not global_config else None
364
+ project_path = str(Path.cwd()) if not global_config else None
331
365
  server_config = create_mcp_server_config(
332
- binary_path=binary_path, project_config=project_config, cwd=cwd
366
+ python_path=python_path,
367
+ project_config=project_config,
368
+ project_path=project_path,
333
369
  )
334
370
 
335
371
  # Step 7: Update MCP configuration
@@ -348,9 +384,10 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
348
384
  console.print("\n[bold]Configuration Details:[/bold]")
349
385
  console.print(" Server name: mcp-ticketer")
350
386
  console.print(f" Adapter: {adapter}")
351
- console.print(f" Binary: {binary_path}")
352
- if cwd:
353
- console.print(f" Working directory: {cwd}")
387
+ console.print(f" Python: {python_path}")
388
+ console.print(" Command: python -m mcp_ticketer.mcp.server")
389
+ if project_path:
390
+ console.print(f" Project path: {project_path}")
354
391
  if "env" in server_config:
355
392
  console.print(
356
393
  f" Environment variables: {list(server_config['env'].keys())}"
@@ -0,0 +1,126 @@
1
+ """Reliable Python executable detection for mcp-ticketer.
2
+
3
+ This module provides reliable detection of the Python executable for mcp-ticketer
4
+ across different installation methods (pipx, pip, uv, direct venv).
5
+
6
+ The module follows the proven pattern from mcp-vector-search:
7
+ - Detect venv Python path reliably
8
+ - Use `python -m mcp_ticketer.mcp.server` instead of binary paths
9
+ - Support multiple installation methods transparently
10
+ """
11
+
12
+ import os
13
+ import shutil
14
+ import sys
15
+ from pathlib import Path
16
+
17
+
18
+ def get_mcp_ticketer_python(project_path: Path | None = None) -> str:
19
+ """Get the correct Python executable for mcp-ticketer MCP server.
20
+
21
+ This function follows the mcp-vector-search pattern of using project-specific
22
+ venv Python for proper project isolation and dependency management.
23
+
24
+ Detection priority:
25
+ 1. Project-local venv (.venv/bin/python) if project_path provided
26
+ 2. Current Python executable if in pipx venv
27
+ 3. Python from mcp-ticketer binary shebang
28
+ 4. Current Python executable (fallback)
29
+
30
+ Args:
31
+ project_path: Optional project directory path to check for local venv
32
+
33
+ Returns:
34
+ Path to Python executable
35
+
36
+ Examples:
37
+ >>> # With project venv
38
+ >>> python_path = get_mcp_ticketer_python(Path("/home/user/my-project"))
39
+ >>> # Returns: "/home/user/my-project/.venv/bin/python"
40
+
41
+ >>> # Without project path (fallback to pipx)
42
+ >>> python_path = get_mcp_ticketer_python()
43
+ >>> # Returns: "/Users/user/.local/pipx/venvs/mcp-ticketer/bin/python"
44
+
45
+ """
46
+ # Priority 1: Check for project-local venv
47
+ if project_path:
48
+ project_venv_python = project_path / ".venv" / "bin" / "python"
49
+ if project_venv_python.exists():
50
+ return str(project_venv_python)
51
+
52
+ current_executable = sys.executable
53
+
54
+ # Priority 2: Check if we're in a pipx venv
55
+ if "/pipx/venvs/" in current_executable:
56
+ return current_executable
57
+
58
+ # Priority 3: Check mcp-ticketer binary shebang
59
+ mcp_ticketer_path = shutil.which("mcp-ticketer")
60
+ if mcp_ticketer_path:
61
+ try:
62
+ with open(mcp_ticketer_path) as f:
63
+ first_line = f.readline().strip()
64
+ if first_line.startswith("#!") and "python" in first_line:
65
+ python_path = first_line[2:].strip()
66
+ if os.path.exists(python_path):
67
+ return python_path
68
+ except OSError:
69
+ pass
70
+
71
+ # Priority 4: Fallback to current Python
72
+ return current_executable
73
+
74
+
75
+ def get_mcp_server_command(project_path: str | None = None) -> tuple[str, list[str]]:
76
+ """Get the complete command to run the MCP server.
77
+
78
+ Args:
79
+ project_path: Optional project path to pass as argument and check for venv
80
+
81
+ Returns:
82
+ Tuple of (python_executable, args_list)
83
+ Example: ("/path/to/python", ["-m", "mcp_ticketer.mcp.server", "/project/path"])
84
+
85
+ Examples:
86
+ >>> python, args = get_mcp_server_command("/home/user/project")
87
+ >>> # python: "/home/user/project/.venv/bin/python" (if .venv exists)
88
+ >>> # args: ["-m", "mcp_ticketer.mcp.server", "/home/user/project"]
89
+
90
+ """
91
+ # Convert project_path to Path object for venv detection
92
+ project_path_obj = Path(project_path) if project_path else None
93
+ python_path = get_mcp_ticketer_python(project_path=project_path_obj)
94
+ args = ["-m", "mcp_ticketer.mcp.server"]
95
+
96
+ if project_path:
97
+ args.append(str(project_path))
98
+
99
+ return python_path, args
100
+
101
+
102
+ def validate_python_executable(python_path: str) -> bool:
103
+ """Validate that a Python executable can import mcp_ticketer.
104
+
105
+ Args:
106
+ python_path: Path to Python executable to validate
107
+
108
+ Returns:
109
+ True if Python can import mcp_ticketer, False otherwise
110
+
111
+ Examples:
112
+ >>> is_valid = validate_python_executable("/usr/bin/python3")
113
+ >>> # Returns: False (system Python doesn't have mcp_ticketer)
114
+
115
+ """
116
+ try:
117
+ import subprocess
118
+
119
+ result = subprocess.run(
120
+ [python_path, "-c", "import mcp_ticketer.mcp.server"],
121
+ capture_output=True,
122
+ timeout=5,
123
+ )
124
+ return result.returncode == 0
125
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
126
+ return False
@@ -1,7 +1,8 @@
1
1
  """Core models and abstractions for MCP Ticketer."""
2
2
 
3
3
  from .adapter import BaseAdapter
4
- from .models import Attachment, Comment, Epic, Priority, Task, TicketState, TicketType
4
+ from .models import (Attachment, Comment, Epic, Priority, Task, TicketState,
5
+ TicketType)
5
6
  from .registry import AdapterRegistry
6
7
 
7
8
  __all__ = [
@@ -1,5 +1,33 @@
1
1
  """MCP server implementation for ticket management."""
2
2
 
3
- from .server import MCPTicketServer
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from .server.main import MCPTicketServer
4
7
 
5
8
  __all__ = ["MCPTicketServer"]
9
+
10
+
11
+ def __dir__():
12
+ """Return list of available names for dir().
13
+
14
+ This ensures that MCPTicketServer appears in dir() results
15
+ even though it's lazily imported.
16
+ """
17
+ return __all__
18
+
19
+
20
+ def __getattr__(name: str):
21
+ """Lazy import to avoid premature module loading.
22
+
23
+ This prevents the RuntimeWarning when running:
24
+ python -m mcp_ticketer.mcp.server
25
+
26
+ The warning occurred because __init__.py imported server before
27
+ runpy could execute it as __main__.
28
+ """
29
+ if name == "MCPTicketServer":
30
+ from .server.main import MCPTicketServer
31
+
32
+ return MCPTicketServer
33
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,60 @@
1
+ """Main entry point for MCP server module invocation.
2
+
3
+ This module enables running the MCP server via:
4
+ python -m mcp_ticketer.mcp.server [project_path]
5
+
6
+ This is the preferred invocation method for MCP configurations as it:
7
+ - Works reliably across installation methods (pipx, pip, uv)
8
+ - Doesn't depend on binary path detection
9
+ - Follows the proven mcp-vector-search pattern
10
+ """
11
+
12
+ import asyncio
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ from .server import main
17
+
18
+
19
+ def run_server() -> None:
20
+ """Run the MCP server with optional project path argument.
21
+
22
+ Usage:
23
+ python -m mcp_ticketer.mcp.server
24
+ python -m mcp_ticketer.mcp.server /path/to/project
25
+
26
+ Arguments:
27
+ project_path (optional): Path to project directory for context
28
+
29
+ """
30
+ # Check for project path argument
31
+ if len(sys.argv) > 1:
32
+ project_path = Path(sys.argv[1])
33
+
34
+ # Validate project path exists
35
+ if not project_path.exists():
36
+ sys.stderr.write(f"Error: Project path does not exist: {project_path}\n")
37
+ sys.exit(1)
38
+
39
+ # Change to project directory for context
40
+ try:
41
+ import os
42
+
43
+ os.chdir(project_path)
44
+ sys.stderr.write(f"[MCP Server] Working directory: {project_path}\n")
45
+ except OSError as e:
46
+ sys.stderr.write(f"Warning: Could not change to project directory: {e}\n")
47
+
48
+ # Run the async main function
49
+ try:
50
+ asyncio.run(main())
51
+ except KeyboardInterrupt:
52
+ sys.stderr.write("\n[MCP Server] Interrupted by user\n")
53
+ sys.exit(0)
54
+ except Exception as e:
55
+ sys.stderr.write(f"[MCP Server] Fatal error: {e}\n")
56
+ sys.exit(1)
57
+
58
+
59
+ if __name__ == "__main__":
60
+ run_server()
@@ -0,0 +1,21 @@
1
+ """MCP Server package for mcp-ticketer.
2
+
3
+ This package provides the FastMCP server implementation for ticket management
4
+ operations via the Model Context Protocol (MCP).
5
+ """
6
+
7
+ from typing import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from .main import main
11
+
12
+ __all__ = ["main"]
13
+
14
+
15
+ def __getattr__(name: str):
16
+ """Lazy import to avoid premature module loading."""
17
+ if name == "main":
18
+ from .main import main
19
+
20
+ return main
21
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,60 @@
1
+ """Main entry point for MCP server module invocation.
2
+
3
+ This module enables running the MCP server via:
4
+ python -m mcp_ticketer.mcp.server [project_path]
5
+
6
+ This is the preferred invocation method for MCP configurations as it:
7
+ - Works reliably across installation methods (pipx, pip, uv)
8
+ - Doesn't depend on binary path detection
9
+ - Follows the proven mcp-vector-search pattern
10
+ """
11
+
12
+ import asyncio
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ from .main import main
17
+
18
+
19
+ def run_server() -> None:
20
+ """Run the MCP server with optional project path argument.
21
+
22
+ Usage:
23
+ python -m mcp_ticketer.mcp.server
24
+ python -m mcp_ticketer.mcp.server /path/to/project
25
+
26
+ Arguments:
27
+ project_path (optional): Path to project directory for context
28
+
29
+ """
30
+ # Check for project path argument
31
+ if len(sys.argv) > 1:
32
+ project_path = Path(sys.argv[1])
33
+
34
+ # Validate project path exists
35
+ if not project_path.exists():
36
+ sys.stderr.write(f"Error: Project path does not exist: {project_path}\n")
37
+ sys.exit(1)
38
+
39
+ # Change to project directory for context
40
+ try:
41
+ import os
42
+
43
+ os.chdir(project_path)
44
+ sys.stderr.write(f"[MCP Server] Working directory: {project_path}\n")
45
+ except OSError as e:
46
+ sys.stderr.write(f"Warning: Could not change to project directory: {e}\n")
47
+
48
+ # Run the async main function
49
+ try:
50
+ asyncio.run(main())
51
+ except KeyboardInterrupt:
52
+ sys.stderr.write("\n[MCP Server] Interrupted by user\n")
53
+ sys.exit(0)
54
+ except Exception as e:
55
+ sys.stderr.write(f"[MCP Server] Fatal error: {e}\n")
56
+ sys.exit(1)
57
+
58
+
59
+ if __name__ == "__main__":
60
+ run_server()
@@ -11,41 +11,21 @@ from dotenv import load_dotenv
11
11
  # Import adapters module to trigger registration
12
12
  import mcp_ticketer.adapters # noqa: F401
13
13
 
14
- from ..core import AdapterRegistry
15
- from ..core.models import Comment, Epic, Priority, SearchQuery, Task, TicketState
16
- from .constants import (
17
- DEFAULT_BASE_PATH,
18
- DEFAULT_LIMIT,
19
- DEFAULT_MAX_DEPTH,
20
- DEFAULT_OFFSET,
21
- ERROR_INTERNAL,
22
- ERROR_METHOD_NOT_FOUND,
23
- ERROR_PARSE,
24
- JSONRPC_VERSION,
25
- MCP_PROTOCOL_VERSION,
26
- MSG_EPIC_NOT_FOUND,
27
- MSG_INTERNAL_ERROR,
28
- MSG_MISSING_TICKET_ID,
29
- MSG_MISSING_TITLE,
30
- MSG_NO_TICKETS_PROVIDED,
31
- MSG_NO_UPDATES_PROVIDED,
32
- MSG_TICKET_NOT_FOUND,
33
- MSG_TRANSITION_FAILED,
34
- MSG_UNKNOWN_METHOD,
35
- MSG_UNKNOWN_OPERATION,
36
- MSG_UPDATE_FAILED,
37
- SERVER_NAME,
38
- SERVER_VERSION,
39
- STATUS_COMPLETED,
40
- STATUS_ERROR,
41
- )
42
- from .dto import (
43
- CreateEpicRequest,
44
- CreateIssueRequest,
45
- CreateTaskRequest,
46
- CreateTicketRequest,
47
- ReadTicketRequest,
48
- )
14
+ from ...core import AdapterRegistry
15
+ from ...core.models import (Comment, Epic, Priority, SearchQuery, Task,
16
+ TicketState)
17
+ from .constants import (DEFAULT_BASE_PATH, DEFAULT_LIMIT, DEFAULT_MAX_DEPTH,
18
+ DEFAULT_OFFSET, ERROR_INTERNAL, ERROR_METHOD_NOT_FOUND,
19
+ ERROR_PARSE, JSONRPC_VERSION, MCP_PROTOCOL_VERSION,
20
+ MSG_EPIC_NOT_FOUND, MSG_INTERNAL_ERROR,
21
+ MSG_MISSING_TICKET_ID, MSG_MISSING_TITLE,
22
+ MSG_NO_TICKETS_PROVIDED, MSG_NO_UPDATES_PROVIDED,
23
+ MSG_TICKET_NOT_FOUND, MSG_TRANSITION_FAILED,
24
+ MSG_UNKNOWN_METHOD, MSG_UNKNOWN_OPERATION,
25
+ MSG_UPDATE_FAILED, SERVER_NAME, SERVER_VERSION,
26
+ STATUS_COMPLETED, STATUS_ERROR)
27
+ from .dto import (CreateEpicRequest, CreateIssueRequest, CreateTaskRequest,
28
+ CreateTicketRequest, ReadTicketRequest)
49
29
  from .response_builder import ResponseBuilder
50
30
 
51
31
  # Load environment variables early (prioritize .env.local)
@@ -17,15 +17,13 @@ Modules:
17
17
 
18
18
  # Import all tool modules to register them with FastMCP
19
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
- )
20
+ from . import attachment_tools # noqa: F401
21
+ from . import bulk_tools # noqa: F401
22
+ from . import comment_tools # noqa: F401
23
+ from . import hierarchy_tools # noqa: F401
24
+ from . import pr_tools # noqa: F401
25
+ from . import search_tools # noqa: F401
26
+ from . import ticket_tools # noqa: F401
29
27
 
30
28
  __all__ = [
31
29
  "ticket_tools",