mcp-ticketer 0.4.4__py3-none-any.whl → 0.4.5__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 +35 -15
- mcp_ticketer/cli/codex_configure.py +38 -31
- mcp_ticketer/cli/gemini_configure.py +38 -25
- mcp_ticketer/cli/mcp_configure.py +115 -78
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/mcp/__init__.py +29 -1
- mcp_ticketer/mcp/__main__.py +60 -0
- mcp_ticketer/queue/__init__.py +3 -1
- mcp_ticketer/queue/manager.py +10 -46
- mcp_ticketer/queue/ticket_registry.py +5 -5
- {mcp_ticketer-0.4.4.dist-info → mcp_ticketer-0.4.5.dist-info}/METADATA +13 -4
- {mcp_ticketer-0.4.4.dist-info → mcp_ticketer-0.4.5.dist-info}/RECORD +17 -15
- {mcp_ticketer-0.4.4.dist-info → mcp_ticketer-0.4.5.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.4.4.dist-info → mcp_ticketer-0.4.5.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.4.4.dist-info → mcp_ticketer-0.4.5.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.4.4.dist-info → mcp_ticketer-0.4.5.dist-info}/top_level.txt +0 -0
mcp_ticketer/__version__.py
CHANGED
|
@@ -10,7 +10,8 @@ from typing import Any
|
|
|
10
10
|
|
|
11
11
|
from rich.console import Console
|
|
12
12
|
|
|
13
|
-
from .mcp_configure import
|
|
13
|
+
from .mcp_configure import load_project_config
|
|
14
|
+
from .python_detection import get_mcp_ticketer_python
|
|
14
15
|
|
|
15
16
|
console = Console()
|
|
16
17
|
|
|
@@ -71,13 +72,14 @@ def save_auggie_config(config_path: Path, config: dict[str, Any]) -> None:
|
|
|
71
72
|
|
|
72
73
|
|
|
73
74
|
def create_auggie_server_config(
|
|
74
|
-
|
|
75
|
+
python_path: str, project_config: dict[str, Any], project_path: str | None = None
|
|
75
76
|
) -> dict[str, Any]:
|
|
76
77
|
"""Create Auggie MCP server configuration for mcp-ticketer.
|
|
77
78
|
|
|
78
79
|
Args:
|
|
79
|
-
|
|
80
|
+
python_path: Path to Python executable in mcp-ticketer venv
|
|
80
81
|
project_config: Project configuration from .mcp-ticketer/config.json
|
|
82
|
+
project_path: Project directory path (optional)
|
|
81
83
|
|
|
82
84
|
Returns:
|
|
83
85
|
Auggie MCP server configuration dict
|
|
@@ -91,6 +93,10 @@ def create_auggie_server_config(
|
|
|
91
93
|
# Build environment variables
|
|
92
94
|
env_vars = {}
|
|
93
95
|
|
|
96
|
+
# Add PYTHONPATH for project context
|
|
97
|
+
if project_path:
|
|
98
|
+
env_vars["PYTHONPATH"] = project_path
|
|
99
|
+
|
|
94
100
|
# Add adapter type
|
|
95
101
|
env_vars["MCP_TICKETER_ADAPTER"] = adapter
|
|
96
102
|
|
|
@@ -128,10 +134,15 @@ def create_auggie_server_config(
|
|
|
128
134
|
if "project_key" in adapter_config:
|
|
129
135
|
env_vars["JIRA_PROJECT_KEY"] = adapter_config["project_key"]
|
|
130
136
|
|
|
137
|
+
# Use module invocation pattern: python -m mcp_ticketer.mcp.server
|
|
138
|
+
args = ["-m", "mcp_ticketer.mcp.server"]
|
|
139
|
+
if project_path:
|
|
140
|
+
args.append(project_path)
|
|
141
|
+
|
|
131
142
|
# Create server configuration (simpler than Gemini - no timeout/trust)
|
|
132
143
|
config = {
|
|
133
|
-
"command":
|
|
134
|
-
"args":
|
|
144
|
+
"command": python_path,
|
|
145
|
+
"args": args,
|
|
135
146
|
"env": env_vars,
|
|
136
147
|
}
|
|
137
148
|
|
|
@@ -214,18 +225,22 @@ def configure_auggie_mcp(force: bool = False) -> None:
|
|
|
214
225
|
force: Overwrite existing configuration
|
|
215
226
|
|
|
216
227
|
Raises:
|
|
217
|
-
FileNotFoundError: If
|
|
228
|
+
FileNotFoundError: If Python executable or project config not found
|
|
218
229
|
ValueError: If configuration is invalid
|
|
219
230
|
|
|
220
231
|
"""
|
|
221
|
-
# Step 1: Find
|
|
222
|
-
console.print("[cyan]🔍 Finding mcp-ticketer
|
|
232
|
+
# Step 1: Find Python executable
|
|
233
|
+
console.print("[cyan]🔍 Finding mcp-ticketer Python executable...[/cyan]")
|
|
223
234
|
try:
|
|
224
|
-
|
|
225
|
-
console.print(f"[green]✓[/green] Found: {
|
|
226
|
-
except
|
|
227
|
-
console.print(f"[red]✗[/red] {e}")
|
|
228
|
-
raise
|
|
235
|
+
python_path = get_mcp_ticketer_python()
|
|
236
|
+
console.print(f"[green]✓[/green] Found: {python_path}")
|
|
237
|
+
except Exception as e:
|
|
238
|
+
console.print(f"[red]✗[/red] Could not find Python executable: {e}")
|
|
239
|
+
raise FileNotFoundError(
|
|
240
|
+
"Could not find mcp-ticketer Python executable. "
|
|
241
|
+
"Please ensure mcp-ticketer is installed.\n"
|
|
242
|
+
"Install with: pip install mcp-ticketer or pipx install mcp-ticketer"
|
|
243
|
+
)
|
|
229
244
|
|
|
230
245
|
# Step 2: Load project configuration
|
|
231
246
|
console.print("\n[cyan]📖 Reading project configuration...[/cyan]")
|
|
@@ -259,8 +274,11 @@ def configure_auggie_mcp(force: bool = False) -> None:
|
|
|
259
274
|
console.print("[yellow]⚠ Overwriting existing configuration[/yellow]")
|
|
260
275
|
|
|
261
276
|
# Step 6: Create mcp-ticketer server config
|
|
277
|
+
project_path = str(Path.cwd())
|
|
262
278
|
server_config = create_auggie_server_config(
|
|
263
|
-
|
|
279
|
+
python_path=python_path,
|
|
280
|
+
project_config=project_config,
|
|
281
|
+
project_path=project_path,
|
|
264
282
|
)
|
|
265
283
|
|
|
266
284
|
# Step 7: Update Auggie configuration
|
|
@@ -279,8 +297,10 @@ def configure_auggie_mcp(force: bool = False) -> None:
|
|
|
279
297
|
console.print("\n[bold]Configuration Details:[/bold]")
|
|
280
298
|
console.print(" Server name: mcp-ticketer")
|
|
281
299
|
console.print(f" Adapter: {adapter}")
|
|
282
|
-
console.print(f"
|
|
300
|
+
console.print(f" Python: {python_path}")
|
|
301
|
+
console.print(" Command: python -m mcp_ticketer.mcp.server")
|
|
283
302
|
console.print(" Scope: Global (affects all projects)")
|
|
303
|
+
console.print(f" Project path: {project_path}")
|
|
284
304
|
if "env" in server_config:
|
|
285
305
|
console.print(
|
|
286
306
|
f" Environment variables: {list(server_config['env'].keys())}"
|
|
@@ -4,19 +4,15 @@ Codex CLI only supports global configuration at ~/.codex/config.toml.
|
|
|
4
4
|
Unlike Claude Code and Gemini CLI, there is no project-level configuration support.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import tomllib
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
|
-
if sys.version_info >= (3, 11):
|
|
12
|
-
import tomllib
|
|
13
|
-
else:
|
|
14
|
-
import tomli as tomllib
|
|
15
|
-
|
|
16
11
|
import tomli_w
|
|
17
12
|
from rich.console import Console
|
|
18
13
|
|
|
19
|
-
from .mcp_configure import
|
|
14
|
+
from .mcp_configure import load_project_config
|
|
15
|
+
from .python_detection import get_mcp_ticketer_python
|
|
20
16
|
|
|
21
17
|
console = Console()
|
|
22
18
|
|
|
@@ -78,14 +74,14 @@ def save_codex_config(config_path: Path, config: dict[str, Any]) -> None:
|
|
|
78
74
|
|
|
79
75
|
|
|
80
76
|
def create_codex_server_config(
|
|
81
|
-
|
|
77
|
+
python_path: str, project_config: dict, project_path: str | None = None
|
|
82
78
|
) -> dict[str, Any]:
|
|
83
79
|
"""Create Codex MCP server configuration for mcp-ticketer.
|
|
84
80
|
|
|
85
81
|
Args:
|
|
86
|
-
|
|
82
|
+
python_path: Path to Python executable in mcp-ticketer venv
|
|
87
83
|
project_config: Project configuration from .mcp-ticketer/config.json
|
|
88
|
-
|
|
84
|
+
project_path: Project directory path (optional, not used for global config)
|
|
89
85
|
|
|
90
86
|
Returns:
|
|
91
87
|
Codex MCP server configuration dict
|
|
@@ -99,9 +95,9 @@ def create_codex_server_config(
|
|
|
99
95
|
# Build environment variables
|
|
100
96
|
env_vars: dict[str, str] = {}
|
|
101
97
|
|
|
102
|
-
# Add PYTHONPATH
|
|
103
|
-
if
|
|
104
|
-
env_vars["PYTHONPATH"] =
|
|
98
|
+
# Add PYTHONPATH for project context
|
|
99
|
+
if project_path:
|
|
100
|
+
env_vars["PYTHONPATH"] = project_path
|
|
105
101
|
|
|
106
102
|
# Add adapter type
|
|
107
103
|
env_vars["MCP_TICKETER_ADAPTER"] = adapter
|
|
@@ -110,9 +106,9 @@ def create_codex_server_config(
|
|
|
110
106
|
if adapter == "aitrackdown":
|
|
111
107
|
# Set base path for local adapter
|
|
112
108
|
base_path = adapter_config.get("base_path", ".aitrackdown")
|
|
113
|
-
if
|
|
114
|
-
# Use absolute path if
|
|
115
|
-
env_vars["MCP_TICKETER_BASE_PATH"] = str(Path(
|
|
109
|
+
if project_path:
|
|
110
|
+
# Use absolute path if project_path is provided
|
|
111
|
+
env_vars["MCP_TICKETER_BASE_PATH"] = str(Path(project_path) / base_path)
|
|
116
112
|
else:
|
|
117
113
|
env_vars["MCP_TICKETER_BASE_PATH"] = base_path
|
|
118
114
|
|
|
@@ -140,11 +136,15 @@ def create_codex_server_config(
|
|
|
140
136
|
if "project_key" in adapter_config:
|
|
141
137
|
env_vars["JIRA_PROJECT_KEY"] = adapter_config["project_key"]
|
|
142
138
|
|
|
139
|
+
# Use module invocation pattern: python -m mcp_ticketer.mcp.server
|
|
140
|
+
args = ["-m", "mcp_ticketer.mcp.server"]
|
|
141
|
+
if project_path:
|
|
142
|
+
args.append(project_path)
|
|
143
|
+
|
|
143
144
|
# Create server configuration with Codex-specific structure
|
|
144
|
-
# NOTE: Codex uses nested dict structure for env vars
|
|
145
145
|
config: dict[str, Any] = {
|
|
146
|
-
"command":
|
|
147
|
-
"args":
|
|
146
|
+
"command": python_path,
|
|
147
|
+
"args": args,
|
|
148
148
|
"env": env_vars,
|
|
149
149
|
}
|
|
150
150
|
|
|
@@ -231,18 +231,22 @@ def configure_codex_mcp(force: bool = False) -> None:
|
|
|
231
231
|
force: Overwrite existing configuration
|
|
232
232
|
|
|
233
233
|
Raises:
|
|
234
|
-
FileNotFoundError: If
|
|
234
|
+
FileNotFoundError: If Python executable or project config not found
|
|
235
235
|
ValueError: If configuration is invalid
|
|
236
236
|
|
|
237
237
|
"""
|
|
238
|
-
# Step 1: Find
|
|
239
|
-
console.print("[cyan]🔍 Finding mcp-ticketer
|
|
238
|
+
# Step 1: Find Python executable
|
|
239
|
+
console.print("[cyan]🔍 Finding mcp-ticketer Python executable...[/cyan]")
|
|
240
240
|
try:
|
|
241
|
-
|
|
242
|
-
console.print(f"[green]✓[/green] Found: {
|
|
243
|
-
except
|
|
244
|
-
console.print(f"[red]✗[/red] {e}")
|
|
245
|
-
raise
|
|
241
|
+
python_path = get_mcp_ticketer_python()
|
|
242
|
+
console.print(f"[green]✓[/green] Found: {python_path}")
|
|
243
|
+
except Exception as e:
|
|
244
|
+
console.print(f"[red]✗[/red] Could not find Python executable: {e}")
|
|
245
|
+
raise FileNotFoundError(
|
|
246
|
+
"Could not find mcp-ticketer Python executable. "
|
|
247
|
+
"Please ensure mcp-ticketer is installed.\n"
|
|
248
|
+
"Install with: pip install mcp-ticketer or pipx install mcp-ticketer"
|
|
249
|
+
)
|
|
246
250
|
|
|
247
251
|
# Step 2: Load project configuration
|
|
248
252
|
console.print("\n[cyan]📖 Reading project configuration...[/cyan]")
|
|
@@ -279,9 +283,11 @@ def configure_codex_mcp(force: bool = False) -> None:
|
|
|
279
283
|
|
|
280
284
|
# Step 6: Create mcp-ticketer server config
|
|
281
285
|
# For global config, include current working directory for context
|
|
282
|
-
|
|
286
|
+
project_path = str(Path.cwd())
|
|
283
287
|
server_config = create_codex_server_config(
|
|
284
|
-
|
|
288
|
+
python_path=python_path,
|
|
289
|
+
project_config=project_config,
|
|
290
|
+
project_path=project_path,
|
|
285
291
|
)
|
|
286
292
|
|
|
287
293
|
# Step 7: Update Codex configuration
|
|
@@ -300,9 +306,10 @@ def configure_codex_mcp(force: bool = False) -> None:
|
|
|
300
306
|
console.print("\n[bold]Configuration Details:[/bold]")
|
|
301
307
|
console.print(" Server name: mcp-ticketer")
|
|
302
308
|
console.print(f" Adapter: {adapter}")
|
|
303
|
-
console.print(f"
|
|
309
|
+
console.print(f" Python: {python_path}")
|
|
310
|
+
console.print(" Command: python -m mcp_ticketer.mcp.server")
|
|
304
311
|
console.print(" Scope: global (Codex only supports global config)")
|
|
305
|
-
console.print(f"
|
|
312
|
+
console.print(f" Project path: {project_path}")
|
|
306
313
|
if "env" in server_config:
|
|
307
314
|
console.print(
|
|
308
315
|
f" Environment variables: {list(server_config['env'].keys())}"
|
|
@@ -6,7 +6,8 @@ from typing import Literal
|
|
|
6
6
|
|
|
7
7
|
from rich.console import Console
|
|
8
8
|
|
|
9
|
-
from .mcp_configure import
|
|
9
|
+
from .mcp_configure import load_project_config
|
|
10
|
+
from .python_detection import get_mcp_ticketer_python
|
|
10
11
|
|
|
11
12
|
console = Console()
|
|
12
13
|
|
|
@@ -73,14 +74,14 @@ def save_gemini_config(config_path: Path, config: dict) -> None:
|
|
|
73
74
|
|
|
74
75
|
|
|
75
76
|
def create_gemini_server_config(
|
|
76
|
-
|
|
77
|
+
python_path: str, project_config: dict, project_path: str | None = None
|
|
77
78
|
) -> dict:
|
|
78
79
|
"""Create Gemini MCP server configuration for mcp-ticketer.
|
|
79
80
|
|
|
80
81
|
Args:
|
|
81
|
-
|
|
82
|
+
python_path: Path to Python executable in mcp-ticketer venv
|
|
82
83
|
project_config: Project configuration from .mcp-ticketer/config.json
|
|
83
|
-
|
|
84
|
+
project_path: Project directory path (optional)
|
|
84
85
|
|
|
85
86
|
Returns:
|
|
86
87
|
Gemini MCP server configuration dict
|
|
@@ -94,9 +95,9 @@ def create_gemini_server_config(
|
|
|
94
95
|
# Build environment variables
|
|
95
96
|
env_vars = {}
|
|
96
97
|
|
|
97
|
-
# Add PYTHONPATH
|
|
98
|
-
if
|
|
99
|
-
env_vars["PYTHONPATH"] =
|
|
98
|
+
# Add PYTHONPATH for project context
|
|
99
|
+
if project_path:
|
|
100
|
+
env_vars["PYTHONPATH"] = project_path
|
|
100
101
|
|
|
101
102
|
# Add adapter type
|
|
102
103
|
env_vars["MCP_TICKETER_ADAPTER"] = adapter
|
|
@@ -105,9 +106,9 @@ def create_gemini_server_config(
|
|
|
105
106
|
if adapter == "aitrackdown":
|
|
106
107
|
# Set base path for local adapter
|
|
107
108
|
base_path = adapter_config.get("base_path", ".aitrackdown")
|
|
108
|
-
if
|
|
109
|
-
# Use absolute path if
|
|
110
|
-
env_vars["MCP_TICKETER_BASE_PATH"] = str(Path(
|
|
109
|
+
if project_path:
|
|
110
|
+
# Use absolute path if project_path is provided
|
|
111
|
+
env_vars["MCP_TICKETER_BASE_PATH"] = str(Path(project_path) / base_path)
|
|
111
112
|
else:
|
|
112
113
|
env_vars["MCP_TICKETER_BASE_PATH"] = base_path
|
|
113
114
|
|
|
@@ -135,10 +136,15 @@ def create_gemini_server_config(
|
|
|
135
136
|
if "project_key" in adapter_config:
|
|
136
137
|
env_vars["JIRA_PROJECT_KEY"] = adapter_config["project_key"]
|
|
137
138
|
|
|
139
|
+
# Use module invocation pattern: python -m mcp_ticketer.mcp.server
|
|
140
|
+
args = ["-m", "mcp_ticketer.mcp.server"]
|
|
141
|
+
if project_path:
|
|
142
|
+
args.append(project_path)
|
|
143
|
+
|
|
138
144
|
# Create server configuration with Gemini-specific options
|
|
139
145
|
config = {
|
|
140
|
-
"command":
|
|
141
|
-
"args":
|
|
146
|
+
"command": python_path,
|
|
147
|
+
"args": args,
|
|
142
148
|
"env": env_vars,
|
|
143
149
|
"timeout": 15000, # 15 seconds timeout
|
|
144
150
|
"trust": False, # Don't trust by default (security)
|
|
@@ -223,18 +229,22 @@ def configure_gemini_mcp(
|
|
|
223
229
|
force: Overwrite existing configuration
|
|
224
230
|
|
|
225
231
|
Raises:
|
|
226
|
-
FileNotFoundError: If
|
|
232
|
+
FileNotFoundError: If Python executable or project config not found
|
|
227
233
|
ValueError: If configuration is invalid
|
|
228
234
|
|
|
229
235
|
"""
|
|
230
|
-
# Step 1: Find
|
|
231
|
-
console.print("[cyan]🔍 Finding mcp-ticketer
|
|
236
|
+
# Step 1: Find Python executable
|
|
237
|
+
console.print("[cyan]🔍 Finding mcp-ticketer Python executable...[/cyan]")
|
|
232
238
|
try:
|
|
233
|
-
|
|
234
|
-
console.print(f"[green]✓[/green] Found: {
|
|
235
|
-
except
|
|
236
|
-
console.print(f"[red]✗[/red] {e}")
|
|
237
|
-
raise
|
|
239
|
+
python_path = get_mcp_ticketer_python()
|
|
240
|
+
console.print(f"[green]✓[/green] Found: {python_path}")
|
|
241
|
+
except Exception as e:
|
|
242
|
+
console.print(f"[red]✗[/red] Could not find Python executable: {e}")
|
|
243
|
+
raise FileNotFoundError(
|
|
244
|
+
"Could not find mcp-ticketer Python executable. "
|
|
245
|
+
"Please ensure mcp-ticketer is installed.\n"
|
|
246
|
+
"Install with: pip install mcp-ticketer or pipx install mcp-ticketer"
|
|
247
|
+
)
|
|
238
248
|
|
|
239
249
|
# Step 2: Load project configuration
|
|
240
250
|
console.print("\n[cyan]📖 Reading project configuration...[/cyan]")
|
|
@@ -266,9 +276,11 @@ def configure_gemini_mcp(
|
|
|
266
276
|
console.print("[yellow]⚠ Overwriting existing configuration[/yellow]")
|
|
267
277
|
|
|
268
278
|
# Step 6: Create mcp-ticketer server config
|
|
269
|
-
|
|
279
|
+
project_path = str(Path.cwd()) if scope == "project" else None
|
|
270
280
|
server_config = create_gemini_server_config(
|
|
271
|
-
|
|
281
|
+
python_path=python_path,
|
|
282
|
+
project_config=project_config,
|
|
283
|
+
project_path=project_path,
|
|
272
284
|
)
|
|
273
285
|
|
|
274
286
|
# Step 7: Update Gemini configuration
|
|
@@ -287,11 +299,12 @@ def configure_gemini_mcp(
|
|
|
287
299
|
console.print("\n[bold]Configuration Details:[/bold]")
|
|
288
300
|
console.print(" Server name: mcp-ticketer")
|
|
289
301
|
console.print(f" Adapter: {adapter}")
|
|
290
|
-
console.print(f"
|
|
302
|
+
console.print(f" Python: {python_path}")
|
|
303
|
+
console.print(" Command: python -m mcp_ticketer.mcp.server")
|
|
291
304
|
console.print(f" Timeout: {server_config['timeout']}ms")
|
|
292
305
|
console.print(f" Trust: {server_config['trust']}")
|
|
293
|
-
if
|
|
294
|
-
console.print(f"
|
|
306
|
+
if project_path:
|
|
307
|
+
console.print(f" Project path: {project_path}")
|
|
295
308
|
if "env" in server_config:
|
|
296
309
|
console.print(
|
|
297
310
|
f" Environment variables: {list(server_config['env'].keys())}"
|
|
@@ -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
|
|
15
|
-
"""
|
|
15
|
+
def load_env_file(env_path: Path) -> dict[str, str]:
|
|
16
|
+
"""Load environment variables from .env file.
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
Path to
|
|
18
|
+
Args:
|
|
19
|
+
env_path: Path to .env file
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
Returns:
|
|
22
|
+
Dict of environment variable key-value pairs
|
|
22
23
|
|
|
23
24
|
"""
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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() / ".
|
|
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
|
-
|
|
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
|
-
|
|
156
|
+
python_path: Path to Python executable in mcp-ticketer venv
|
|
179
157
|
project_config: Project configuration from .mcp-ticketer/config.json
|
|
180
|
-
|
|
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
|
-
"
|
|
188
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
308
|
+
FileNotFoundError: If Python executable or project config not found
|
|
288
309
|
ValueError: If configuration is invalid
|
|
289
310
|
|
|
290
311
|
"""
|
|
291
|
-
#
|
|
292
|
-
|
|
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
|
-
|
|
295
|
-
console.print(f"[green]✓[/green] Found: {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
364
|
+
project_path = str(Path.cwd()) if not global_config else None
|
|
331
365
|
server_config = create_mcp_server_config(
|
|
332
|
-
|
|
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"
|
|
352
|
-
|
|
353
|
-
|
|
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
|
mcp_ticketer/mcp/__init__.py
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
"""MCP server implementation for ticket management."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .server 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.py before
|
|
27
|
+
runpy could execute it as __main__.
|
|
28
|
+
"""
|
|
29
|
+
if name == "MCPTicketServer":
|
|
30
|
+
from .server 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()
|
mcp_ticketer/queue/__init__.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"""Async queue system for mcp-ticketer."""
|
|
2
2
|
|
|
3
|
-
from .manager import WorkerManager
|
|
4
3
|
from .queue import Queue, QueueItem, QueueStatus
|
|
5
4
|
from .worker import Worker
|
|
6
5
|
|
|
6
|
+
# Import manager last to avoid circular import
|
|
7
|
+
from .manager import WorkerManager
|
|
8
|
+
|
|
7
9
|
__all__ = ["Queue", "QueueItem", "QueueStatus", "Worker", "WorkerManager"]
|
mcp_ticketer/queue/manager.py
CHANGED
|
@@ -4,14 +4,14 @@ import fcntl
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
import subprocess
|
|
7
|
-
import sys
|
|
8
7
|
import time
|
|
9
8
|
from pathlib import Path
|
|
10
|
-
from typing import Any
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
11
10
|
|
|
12
11
|
import psutil
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .queue import Queue
|
|
15
15
|
|
|
16
16
|
logger = logging.getLogger(__name__)
|
|
17
17
|
|
|
@@ -21,6 +21,9 @@ class WorkerManager:
|
|
|
21
21
|
|
|
22
22
|
def __init__(self):
|
|
23
23
|
"""Initialize worker manager."""
|
|
24
|
+
# Lazy import to avoid circular dependency
|
|
25
|
+
from .queue import Queue
|
|
26
|
+
|
|
24
27
|
self.lock_file = Path.home() / ".mcp-ticketer" / "worker.lock"
|
|
25
28
|
self.pid_file = Path.home() / ".mcp-ticketer" / "worker.pid"
|
|
26
29
|
self.lock_file.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -123,7 +126,10 @@ class WorkerManager:
|
|
|
123
126
|
try:
|
|
124
127
|
# Start worker in subprocess using the same Python executable as the CLI
|
|
125
128
|
# This ensures the worker can import mcp_ticketer modules
|
|
126
|
-
|
|
129
|
+
# Lazy import to avoid circular dependency
|
|
130
|
+
from ..cli.python_detection import get_mcp_ticketer_python
|
|
131
|
+
|
|
132
|
+
python_executable = get_mcp_ticketer_python()
|
|
127
133
|
cmd = [python_executable, "-m", "mcp_ticketer.queue.run_worker"]
|
|
128
134
|
|
|
129
135
|
# Prepare environment for subprocess
|
|
@@ -320,48 +326,6 @@ class WorkerManager:
|
|
|
320
326
|
except (OSError, ValueError):
|
|
321
327
|
return None
|
|
322
328
|
|
|
323
|
-
def _get_python_executable(self) -> str:
|
|
324
|
-
"""Get the correct Python executable for the worker subprocess.
|
|
325
|
-
|
|
326
|
-
This ensures the worker uses the same Python environment as the CLI,
|
|
327
|
-
which is critical for module imports to work correctly.
|
|
328
|
-
|
|
329
|
-
Returns:
|
|
330
|
-
Path to Python executable
|
|
331
|
-
|
|
332
|
-
"""
|
|
333
|
-
# First, try to detect if we're running in a pipx environment
|
|
334
|
-
# by checking if the current executable is in a pipx venv
|
|
335
|
-
current_executable = sys.executable
|
|
336
|
-
|
|
337
|
-
# Check if we're in a pipx venv (path contains /pipx/venvs/)
|
|
338
|
-
if "/pipx/venvs/" in current_executable:
|
|
339
|
-
logger.debug(f"Using pipx Python executable: {current_executable}")
|
|
340
|
-
return current_executable
|
|
341
|
-
|
|
342
|
-
# Check if we can find the mcp-ticketer executable and extract its Python
|
|
343
|
-
import shutil
|
|
344
|
-
|
|
345
|
-
mcp_ticketer_path = shutil.which("mcp-ticketer")
|
|
346
|
-
if mcp_ticketer_path:
|
|
347
|
-
try:
|
|
348
|
-
# Read the shebang line to get the Python executable
|
|
349
|
-
with open(mcp_ticketer_path) as f:
|
|
350
|
-
first_line = f.readline().strip()
|
|
351
|
-
if first_line.startswith("#!") and "python" in first_line:
|
|
352
|
-
python_path = first_line[2:].strip()
|
|
353
|
-
if os.path.exists(python_path):
|
|
354
|
-
logger.debug(
|
|
355
|
-
f"Using Python from mcp-ticketer shebang: {python_path}"
|
|
356
|
-
)
|
|
357
|
-
return python_path
|
|
358
|
-
except OSError:
|
|
359
|
-
pass
|
|
360
|
-
|
|
361
|
-
# Fallback to sys.executable
|
|
362
|
-
logger.debug(f"Using sys.executable as fallback: {current_executable}")
|
|
363
|
-
return current_executable
|
|
364
|
-
|
|
365
329
|
def _cleanup(self):
|
|
366
330
|
"""Clean up lock and PID files."""
|
|
367
331
|
self._release_lock()
|
|
@@ -202,7 +202,7 @@ class TicketRegistry:
|
|
|
202
202
|
return None
|
|
203
203
|
|
|
204
204
|
columns = [desc[0] for desc in cursor.description]
|
|
205
|
-
ticket_info = dict(zip(columns, row))
|
|
205
|
+
ticket_info = dict(zip(columns, row, strict=False))
|
|
206
206
|
|
|
207
207
|
# Parse JSON fields
|
|
208
208
|
if ticket_info.get("ticket_data"):
|
|
@@ -236,7 +236,7 @@ class TicketRegistry:
|
|
|
236
236
|
columns = [desc[0] for desc in cursor.description]
|
|
237
237
|
|
|
238
238
|
for row in cursor.fetchall():
|
|
239
|
-
ticket_info = dict(zip(columns, row))
|
|
239
|
+
ticket_info = dict(zip(columns, row, strict=False))
|
|
240
240
|
|
|
241
241
|
# Parse JSON fields
|
|
242
242
|
if ticket_info.get("ticket_data"):
|
|
@@ -273,7 +273,7 @@ class TicketRegistry:
|
|
|
273
273
|
columns = [desc[0] for desc in cursor.description]
|
|
274
274
|
|
|
275
275
|
for row in cursor.fetchall():
|
|
276
|
-
ticket_info = dict(zip(columns, row))
|
|
276
|
+
ticket_info = dict(zip(columns, row, strict=False))
|
|
277
277
|
|
|
278
278
|
# Parse JSON fields
|
|
279
279
|
if ticket_info.get("ticket_data"):
|
|
@@ -306,7 +306,7 @@ class TicketRegistry:
|
|
|
306
306
|
columns = [desc[0] for desc in cursor.description]
|
|
307
307
|
|
|
308
308
|
for row in cursor.fetchall():
|
|
309
|
-
ticket_info = dict(zip(columns, row))
|
|
309
|
+
ticket_info = dict(zip(columns, row, strict=False))
|
|
310
310
|
|
|
311
311
|
# Parse JSON fields
|
|
312
312
|
if ticket_info.get("ticket_data"):
|
|
@@ -436,7 +436,7 @@ class TicketRegistry:
|
|
|
436
436
|
columns = [desc[0] for desc in cursor.description]
|
|
437
437
|
|
|
438
438
|
for row in cursor.fetchall():
|
|
439
|
-
recovery_info = dict(zip(columns, row))
|
|
439
|
+
recovery_info = dict(zip(columns, row, strict=False))
|
|
440
440
|
if recovery_info.get("recovery_data"):
|
|
441
441
|
recovery_info["recovery_data"] = json.loads(
|
|
442
442
|
recovery_info["recovery_data"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-ticketer
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.5
|
|
4
4
|
Summary: Universal ticket management interface for AI agents with MCP support
|
|
5
5
|
Author-email: MCP Ticketer Team <support@mcp-ticketer.io>
|
|
6
6
|
Maintainer-email: MCP Ticketer Team <support@mcp-ticketer.io>
|
|
@@ -282,16 +282,25 @@ mcp-ticketer uninstall auggie # Alias for remove
|
|
|
282
282
|
{
|
|
283
283
|
"mcpServers": {
|
|
284
284
|
"mcp-ticketer": {
|
|
285
|
-
"command": "/path/to/
|
|
286
|
-
"args": ["
|
|
285
|
+
"command": "/path/to/venv/bin/python",
|
|
286
|
+
"args": ["-m", "mcp_ticketer.mcp.server", "/absolute/path/to/project"],
|
|
287
287
|
"env": {
|
|
288
|
-
"MCP_TICKETER_ADAPTER": "aitrackdown"
|
|
288
|
+
"MCP_TICKETER_ADAPTER": "aitrackdown",
|
|
289
|
+
"PYTHONPATH": "/absolute/path/to/project"
|
|
289
290
|
}
|
|
290
291
|
}
|
|
291
292
|
}
|
|
292
293
|
}
|
|
293
294
|
```
|
|
294
295
|
|
|
296
|
+
**Why this pattern?**
|
|
297
|
+
- **More Reliable**: Uses venv Python directly instead of binary wrapper
|
|
298
|
+
- **Consistent**: Matches proven mcp-vector-search pattern
|
|
299
|
+
- **Universal**: Works across pipx, pip, and uv installations
|
|
300
|
+
- **Better Errors**: Python module invocation provides clearer error messages
|
|
301
|
+
|
|
302
|
+
**Automatic Detection**: The `mcp-ticketer install` commands automatically detect your venv Python and generate the correct configuration.
|
|
303
|
+
|
|
295
304
|
**See [AI Client Integration Guide](docs/AI_CLIENT_INTEGRATION.md) for client-specific details.**
|
|
296
305
|
|
|
297
306
|
## ⚙️ Configuration
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
mcp_ticketer/__init__.py,sha256=Xx4WaprO5PXhVPbYi1L6tBmwmJMkYS-lMyG4ieN6QP0,717
|
|
2
|
-
mcp_ticketer/__version__.py,sha256=
|
|
2
|
+
mcp_ticketer/__version__.py,sha256=vioHFC2eLXv3RlmwMaPO8QGtnhB8gou02LbH7LA6A-U,1117
|
|
3
3
|
mcp_ticketer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
mcp_ticketer/adapters/__init__.py,sha256=B5DFllWn23hkhmrLykNO5uMMSdcFuuPHXyLw_jyFzuE,358
|
|
5
5
|
mcp_ticketer/adapters/aitrackdown.py,sha256=qCR53JpOVlOHiR_pdjGF2RYOemgkKxIwDTOlpcYF18E,30320
|
|
@@ -17,17 +17,18 @@ mcp_ticketer/cache/__init__.py,sha256=Xcd-cKnt-Cx7jBzvfzUUUPaGkmyXFi5XUFWw3Z4b7d
|
|
|
17
17
|
mcp_ticketer/cache/memory.py,sha256=rWphWZy7XTbHezC7HMRQN9ISUhYo0Pc2OTgLG30vHnI,5047
|
|
18
18
|
mcp_ticketer/cli/__init__.py,sha256=l9Q8iKmfGkTu0cssHBVqNZTsL4eAtFzOB25AED_0G6g,89
|
|
19
19
|
mcp_ticketer/cli/adapter_diagnostics.py,sha256=pQDdtDgBwSW04wdFEPVzwbul3KgfB9g6ZMS85qpYulY,14988
|
|
20
|
-
mcp_ticketer/cli/auggie_configure.py,sha256=
|
|
21
|
-
mcp_ticketer/cli/codex_configure.py,sha256=
|
|
20
|
+
mcp_ticketer/cli/auggie_configure.py,sha256=BA31HvOXljPqi3QMKt5eI5jYUWCnnH00sDqwR3y2nSY,11701
|
|
21
|
+
mcp_ticketer/cli/codex_configure.py,sha256=LLGzsFjCNO3irtabSazCpsZ5eUmG6eAjCNn6B5M4aHQ,12249
|
|
22
22
|
mcp_ticketer/cli/configure.py,sha256=T4LczvZIc2FZM-joqICL8NpjCeThAUknEhJkHsmpdc8,16158
|
|
23
23
|
mcp_ticketer/cli/diagnostics.py,sha256=s7P4vDzPpthgiBJfAOXpunlhZ62buHg_BA5W7ghQBqg,29952
|
|
24
24
|
mcp_ticketer/cli/discover.py,sha256=paG8zElIFEK6lgVD-tHWoFDTuWQ185LirFp0fVlYgB0,13148
|
|
25
|
-
mcp_ticketer/cli/gemini_configure.py,sha256=
|
|
25
|
+
mcp_ticketer/cli/gemini_configure.py,sha256=GXbQkfZGsmx_2cJKxH_5z3JdcwpMopF0duI_TzbKip8,12644
|
|
26
26
|
mcp_ticketer/cli/linear_commands.py,sha256=YnmqRacAfTdF0CLr4BXOxFs3uwm_hVifbGdTe6t2CfA,17273
|
|
27
27
|
mcp_ticketer/cli/main.py,sha256=DV05n5-JGIjIX1t4-oR7j5tuE4c2Z0of6KVahUzGiJY,89254
|
|
28
|
-
mcp_ticketer/cli/mcp_configure.py,sha256=
|
|
28
|
+
mcp_ticketer/cli/mcp_configure.py,sha256=dgApMLBApLxU0fvFOyDBkX69GOGVNTXhAKM-hZGw5v8,14142
|
|
29
29
|
mcp_ticketer/cli/migrate_config.py,sha256=MYsr_C5ZxsGg0P13etWTWNrJ_lc6ElRCkzfQADYr3DM,5956
|
|
30
30
|
mcp_ticketer/cli/platform_commands.py,sha256=pTLRT2wot8dAmy1-roJWWOT0Cxu7j-06BlWDnZ9a4jY,3624
|
|
31
|
+
mcp_ticketer/cli/python_detection.py,sha256=qmhi0CIDKH_AUVGkJ9jyY1zBpx1cwiQNv0vnEvMYDFQ,4272
|
|
31
32
|
mcp_ticketer/cli/queue_commands.py,sha256=mm-3H6jmkUGJDyU_E46o9iRpek8tvFCm77F19OtHiZI,7884
|
|
32
33
|
mcp_ticketer/cli/simple_health.py,sha256=GlOLRRFoifCna995NoHuKpb3xmFkLi2b3Ke1hyeDvq4,7950
|
|
33
34
|
mcp_ticketer/cli/ticket_commands.py,sha256=zmtePGhZzhw_r-0xWQMzXSJMo684PlzEN5LYfcR3_dw,26589
|
|
@@ -43,7 +44,8 @@ mcp_ticketer/core/mappers.py,sha256=okte6EV_OuPvnM1KXHUcfrpPd7TWnKh44X3_W3HxwiI,
|
|
|
43
44
|
mcp_ticketer/core/models.py,sha256=ABKdyAkEkKtMF_d6D8_qRL-2ujz1DshemHSyqTPUthA,14448
|
|
44
45
|
mcp_ticketer/core/project_config.py,sha256=DmLekuMuOgNtzg-olOU4Utv00DdCH1-CXuoooA-adMs,23609
|
|
45
46
|
mcp_ticketer/core/registry.py,sha256=gBeXcZ3grHl9gYFbyRp-C4IM7SD_KGTeXT_1jG8XrCc,3470
|
|
46
|
-
mcp_ticketer/mcp/__init__.py,sha256=
|
|
47
|
+
mcp_ticketer/mcp/__init__.py,sha256=fWcCqL7MxKcsnq8vUqDV-rsxykOo3J5_98bba1oT1dw,857
|
|
48
|
+
mcp_ticketer/mcp/__main__.py,sha256=Fo_5KJOFako2gi1Z1kk5zEt2sGJW6BX6oXlYp7twYTs,1713
|
|
47
49
|
mcp_ticketer/mcp/constants.py,sha256=EBGsJtBPaTCvAm5rOMknckrXActrNIls7lRklnh1L4s,2072
|
|
48
50
|
mcp_ticketer/mcp/dto.py,sha256=FR_OBtaxrno8AsHynPwUUW715iAoaBkrr7Ud8HZTQW8,7233
|
|
49
51
|
mcp_ticketer/mcp/response_builder.py,sha256=DUfe1e0CcXPlepLq-cGH6b_THqoZEynYfVKkZEeLe0M,4933
|
|
@@ -57,17 +59,17 @@ mcp_ticketer/mcp/tools/hierarchy_tools.py,sha256=08KxOawUKGZK3kYPZ5w9mRk-lVaSFxQ
|
|
|
57
59
|
mcp_ticketer/mcp/tools/pr_tools.py,sha256=SnKJRsvkmmr2eh1qaIqxC0PHD6rVXKg6mkk9Skdpc0Y,4539
|
|
58
60
|
mcp_ticketer/mcp/tools/search_tools.py,sha256=6qwSBRkGx-zO0rxebeQqMcBj006ZovAgfyLGfw3kxig,6951
|
|
59
61
|
mcp_ticketer/mcp/tools/ticket_tools.py,sha256=KYMl2h-nf7hZq-kOi1z4H0TEj7MzMklOsRIl4HaP8WQ,8121
|
|
60
|
-
mcp_ticketer/queue/__init__.py,sha256=
|
|
62
|
+
mcp_ticketer/queue/__init__.py,sha256=yQtdFNHhk1G_BHTuYk-Vlu6ZgEQ6Ik994zMH1IGHwbc,279
|
|
61
63
|
mcp_ticketer/queue/__main__.py,sha256=gc_tE9NUdK07OJfTZuD4t6KeBD_vxFQIhknGTQUG_jk,109
|
|
62
64
|
mcp_ticketer/queue/health_monitor.py,sha256=TDmPnYuZJb3yHNJlGFvE9UU-LfsKTrC4Vapyvdb3fso,12226
|
|
63
|
-
mcp_ticketer/queue/manager.py,sha256=
|
|
65
|
+
mcp_ticketer/queue/manager.py,sha256=UHkI0OSDyspY0EM2UVf5E1PjUTmZIcOcgGNovxtuVYs,10732
|
|
64
66
|
mcp_ticketer/queue/queue.py,sha256=q9HDXgnlwspamMJIeu9og7qONttXHmFZHPSaMtJDPlw,17923
|
|
65
67
|
mcp_ticketer/queue/run_worker.py,sha256=WhoeamL8LKZ66TM8W1PkMPwjF2w_EDFMP-mevs6C1TM,1019
|
|
66
|
-
mcp_ticketer/queue/ticket_registry.py,sha256=
|
|
68
|
+
mcp_ticketer/queue/ticket_registry.py,sha256=xVg3i7Eb5rtQY-4bbw3zYY1h-C6jF1t1NZEGhObzD8g,15491
|
|
67
69
|
mcp_ticketer/queue/worker.py,sha256=AJHtpJZEhGoPuCDPXSMsn9DeODelo5f__0C__3zoN08,20970
|
|
68
|
-
mcp_ticketer-0.4.
|
|
69
|
-
mcp_ticketer-0.4.
|
|
70
|
-
mcp_ticketer-0.4.
|
|
71
|
-
mcp_ticketer-0.4.
|
|
72
|
-
mcp_ticketer-0.4.
|
|
73
|
-
mcp_ticketer-0.4.
|
|
70
|
+
mcp_ticketer-0.4.5.dist-info/licenses/LICENSE,sha256=KOVrunjtILSzY-2N8Lqa3-Q8dMaZIG4LrlLTr9UqL08,1073
|
|
71
|
+
mcp_ticketer-0.4.5.dist-info/METADATA,sha256=l1eDYLelVRQ1aJldM9nXa0_vvelSgQ6o78smdDlCYjo,16020
|
|
72
|
+
mcp_ticketer-0.4.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
73
|
+
mcp_ticketer-0.4.5.dist-info/entry_points.txt,sha256=o1IxVhnHnBNG7FZzbFq-Whcs1Djbofs0qMjiUYBLx2s,60
|
|
74
|
+
mcp_ticketer-0.4.5.dist-info/top_level.txt,sha256=WnAG4SOT1Vm9tIwl70AbGG_nA217YyV3aWFhxLH2rxw,13
|
|
75
|
+
mcp_ticketer-0.4.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|