mcp-ticketer 0.3.5__py3-none-any.whl → 0.12.0__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 +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +263 -14
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1308 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +334 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +326 -109
- mcp_ticketer/adapters/hybrid.py +11 -11
- mcp_ticketer/adapters/jira.py +271 -25
- mcp_ticketer/adapters/linear/adapter.py +693 -39
- mcp_ticketer/adapters/linear/client.py +61 -9
- mcp_ticketer/adapters/linear/mappers.py +9 -3
- mcp_ticketer/adapters/linear/queries.py +9 -7
- mcp_ticketer/cache/memory.py +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +1 -1
- mcp_ticketer/cli/auggie_configure.py +104 -15
- mcp_ticketer/cli/codex_configure.py +188 -32
- mcp_ticketer/cli/configure.py +37 -48
- mcp_ticketer/cli/diagnostics.py +20 -18
- mcp_ticketer/cli/discover.py +292 -26
- mcp_ticketer/cli/gemini_configure.py +107 -26
- mcp_ticketer/cli/instruction_commands.py +429 -0
- mcp_ticketer/cli/linear_commands.py +105 -22
- mcp_ticketer/cli/main.py +1830 -435
- mcp_ticketer/cli/mcp_configure.py +296 -89
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +412 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/simple_health.py +1 -1
- mcp_ticketer/cli/ticket_commands.py +773 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +67 -62
- mcp_ticketer/core/__init__.py +14 -1
- mcp_ticketer/core/adapter.py +84 -15
- mcp_ticketer/core/config.py +44 -39
- mcp_ticketer/core/env_discovery.py +42 -12
- mcp_ticketer/core/env_loader.py +15 -14
- mcp_ticketer/core/exceptions.py +3 -3
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/mappers.py +11 -11
- mcp_ticketer/core/models.py +50 -20
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/project_config.py +57 -35
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +29 -1
- mcp_ticketer/mcp/__main__.py +60 -0
- mcp_ticketer/mcp/server/__init__.py +25 -0
- mcp_ticketer/mcp/server/__main__.py +60 -0
- mcp_ticketer/mcp/{dto.py → server/dto.py} +32 -32
- mcp_ticketer/mcp/{server.py → server/main.py} +127 -74
- mcp_ticketer/mcp/{response_builder.py → server/response_builder.py} +2 -2
- mcp_ticketer/mcp/server/server_sdk.py +93 -0
- mcp_ticketer/mcp/server/tools/__init__.py +47 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +90 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +532 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +154 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +206 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +430 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +5 -4
- mcp_ticketer/queue/manager.py +15 -51
- mcp_ticketer/queue/queue.py +19 -19
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +14 -14
- mcp_ticketer/queue/worker.py +16 -14
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +168 -32
- mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
- mcp_ticketer-0.3.5.dist-info/RECORD +0 -62
- /mcp_ticketer/mcp/{constants.py → server/constants.py} +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/top_level.txt +0 -0
|
@@ -2,63 +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
|
-
from typing import Optional
|
|
9
7
|
|
|
10
8
|
from rich.console import Console
|
|
11
9
|
|
|
10
|
+
from .python_detection import get_mcp_ticketer_python
|
|
11
|
+
|
|
12
12
|
console = Console()
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
def
|
|
16
|
-
"""
|
|
15
|
+
def load_env_file(env_path: Path) -> dict[str, str]:
|
|
16
|
+
"""Load environment variables from .env file.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
Path to
|
|
18
|
+
Args:
|
|
19
|
+
env_path: Path to .env file
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
Returns:
|
|
22
|
+
Dict of environment variable key-value pairs
|
|
23
23
|
|
|
24
24
|
"""
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
/ "venvs"
|
|
43
|
-
/ "mcp-ticketer"
|
|
44
|
-
/ "bin"
|
|
45
|
-
/ "mcp-ticketer",
|
|
46
|
-
]
|
|
47
|
-
|
|
48
|
-
# Check PATH
|
|
49
|
-
which_result = shutil.which("mcp-ticketer")
|
|
50
|
-
if which_result:
|
|
51
|
-
return which_result
|
|
52
|
-
|
|
53
|
-
# Check possible paths
|
|
54
|
-
for path in possible_paths:
|
|
55
|
-
if path.exists():
|
|
56
|
-
return str(path.resolve())
|
|
57
|
-
|
|
58
|
-
raise FileNotFoundError(
|
|
59
|
-
"Could not find mcp-ticketer binary. Please ensure mcp-ticketer is installed.\n"
|
|
60
|
-
"Install with: pip install mcp-ticketer"
|
|
61
|
-
)
|
|
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
|
|
62
42
|
|
|
63
43
|
|
|
64
44
|
def load_project_config() -> dict:
|
|
@@ -127,28 +107,44 @@ def find_claude_mcp_config(global_config: bool = False) -> Path:
|
|
|
127
107
|
Path.home() / ".config" / "Claude" / "claude_desktop_config.json"
|
|
128
108
|
)
|
|
129
109
|
else:
|
|
130
|
-
#
|
|
131
|
-
config_path = Path.
|
|
110
|
+
# Claude Code configuration (project-specific)
|
|
111
|
+
config_path = Path.home() / ".claude.json"
|
|
132
112
|
|
|
133
113
|
return config_path
|
|
134
114
|
|
|
135
115
|
|
|
136
|
-
def load_claude_mcp_config(config_path: Path) -> dict:
|
|
116
|
+
def load_claude_mcp_config(config_path: Path, is_claude_code: bool = False) -> dict:
|
|
137
117
|
"""Load existing Claude MCP configuration or return empty structure.
|
|
138
118
|
|
|
139
119
|
Args:
|
|
140
120
|
config_path: Path to MCP config file
|
|
121
|
+
is_claude_code: If True, return Claude Code structure with projects
|
|
141
122
|
|
|
142
123
|
Returns:
|
|
143
124
|
MCP configuration dict
|
|
144
125
|
|
|
145
126
|
"""
|
|
146
127
|
if config_path.exists():
|
|
147
|
-
|
|
148
|
-
|
|
128
|
+
try:
|
|
129
|
+
with open(config_path) as f:
|
|
130
|
+
content = f.read().strip()
|
|
131
|
+
if not content:
|
|
132
|
+
# Empty file, return default structure
|
|
133
|
+
return {"projects": {}} if is_claude_code else {"mcpServers": {}}
|
|
134
|
+
return json.loads(content)
|
|
135
|
+
except json.JSONDecodeError as e:
|
|
136
|
+
console.print(
|
|
137
|
+
f"[yellow]⚠ Warning: Invalid JSON in {config_path}, creating new config[/yellow]"
|
|
138
|
+
)
|
|
139
|
+
console.print(f"[dim]Error: {e}[/dim]")
|
|
140
|
+
# Return default structure on parse error
|
|
141
|
+
return {"projects": {}} if is_claude_code else {"mcpServers": {}}
|
|
149
142
|
|
|
150
|
-
# Return empty structure
|
|
151
|
-
|
|
143
|
+
# Return empty structure based on config type
|
|
144
|
+
if is_claude_code:
|
|
145
|
+
return {"projects": {}}
|
|
146
|
+
else:
|
|
147
|
+
return {"mcpServers": {}}
|
|
152
148
|
|
|
153
149
|
|
|
154
150
|
def save_claude_mcp_config(config_path: Path, config: dict) -> None:
|
|
@@ -168,28 +164,33 @@ def save_claude_mcp_config(config_path: Path, config: dict) -> None:
|
|
|
168
164
|
|
|
169
165
|
|
|
170
166
|
def create_mcp_server_config(
|
|
171
|
-
|
|
167
|
+
python_path: str, project_config: dict, project_path: str | None = None
|
|
172
168
|
) -> dict:
|
|
173
169
|
"""Create MCP server configuration for mcp-ticketer.
|
|
174
170
|
|
|
175
171
|
Args:
|
|
176
|
-
|
|
172
|
+
python_path: Path to Python executable in mcp-ticketer venv
|
|
177
173
|
project_config: Project configuration from .mcp-ticketer/config.json
|
|
178
|
-
|
|
174
|
+
project_path: Project directory path (optional)
|
|
179
175
|
|
|
180
176
|
Returns:
|
|
181
|
-
MCP server configuration dict
|
|
177
|
+
MCP server configuration dict matching Claude Code stdio pattern
|
|
182
178
|
|
|
183
179
|
"""
|
|
180
|
+
# Use Python module invocation pattern (works regardless of where package is installed)
|
|
181
|
+
args = ["-m", "mcp_ticketer.mcp.server"]
|
|
182
|
+
|
|
183
|
+
# Add project path if provided
|
|
184
|
+
if project_path:
|
|
185
|
+
args.append(project_path)
|
|
186
|
+
|
|
187
|
+
# REQUIRED: Add "type": "stdio" for Claude Code compatibility
|
|
184
188
|
config = {
|
|
185
|
-
"
|
|
186
|
-
"
|
|
189
|
+
"type": "stdio",
|
|
190
|
+
"command": python_path,
|
|
191
|
+
"args": args,
|
|
187
192
|
}
|
|
188
193
|
|
|
189
|
-
# Add working directory if provided
|
|
190
|
-
if cwd:
|
|
191
|
-
config["cwd"] = cwd
|
|
192
|
-
|
|
193
194
|
# Add environment variables based on adapter
|
|
194
195
|
adapter = project_config.get("default_adapter", "aitrackdown")
|
|
195
196
|
adapters_config = project_config.get("adapters", {})
|
|
@@ -197,15 +198,49 @@ def create_mcp_server_config(
|
|
|
197
198
|
|
|
198
199
|
env_vars = {}
|
|
199
200
|
|
|
200
|
-
# Add
|
|
201
|
+
# Add PYTHONPATH for project context
|
|
202
|
+
if project_path:
|
|
203
|
+
env_vars["PYTHONPATH"] = project_path
|
|
204
|
+
|
|
205
|
+
# Add MCP_TICKETER_ADAPTER to identify which adapter to use
|
|
206
|
+
env_vars["MCP_TICKETER_ADAPTER"] = adapter
|
|
207
|
+
|
|
208
|
+
# Load environment variables from .env.local if it exists
|
|
209
|
+
if project_path:
|
|
210
|
+
env_file_path = Path(project_path) / ".env.local"
|
|
211
|
+
env_file_vars = load_env_file(env_file_path)
|
|
212
|
+
|
|
213
|
+
# Add relevant adapter-specific vars from .env.local
|
|
214
|
+
adapter_env_keys = {
|
|
215
|
+
"linear": ["LINEAR_API_KEY", "LINEAR_TEAM_ID", "LINEAR_TEAM_KEY"],
|
|
216
|
+
"github": ["GITHUB_TOKEN", "GITHUB_OWNER", "GITHUB_REPO"],
|
|
217
|
+
"jira": [
|
|
218
|
+
"JIRA_ACCESS_USER",
|
|
219
|
+
"JIRA_ACCESS_TOKEN",
|
|
220
|
+
"JIRA_ORGANIZATION_ID",
|
|
221
|
+
"JIRA_URL",
|
|
222
|
+
"JIRA_EMAIL",
|
|
223
|
+
"JIRA_API_TOKEN",
|
|
224
|
+
],
|
|
225
|
+
"aitrackdown": [], # No specific env vars needed
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
# Include adapter-specific env vars from .env.local
|
|
229
|
+
for key in adapter_env_keys.get(adapter, []):
|
|
230
|
+
if key in env_file_vars:
|
|
231
|
+
env_vars[key] = env_file_vars[key]
|
|
232
|
+
|
|
233
|
+
# Fallback: Add adapter-specific environment variables from project config
|
|
201
234
|
if adapter == "linear" and "api_key" in adapter_config:
|
|
202
|
-
|
|
235
|
+
if "LINEAR_API_KEY" not in env_vars:
|
|
236
|
+
env_vars["LINEAR_API_KEY"] = adapter_config["api_key"]
|
|
203
237
|
elif adapter == "github" and "token" in adapter_config:
|
|
204
|
-
|
|
238
|
+
if "GITHUB_TOKEN" not in env_vars:
|
|
239
|
+
env_vars["GITHUB_TOKEN"] = adapter_config["token"]
|
|
205
240
|
elif adapter == "jira":
|
|
206
|
-
if "api_token" in adapter_config:
|
|
241
|
+
if "api_token" in adapter_config and "JIRA_API_TOKEN" not in env_vars:
|
|
207
242
|
env_vars["JIRA_API_TOKEN"] = adapter_config["api_token"]
|
|
208
|
-
if "email" in adapter_config:
|
|
243
|
+
if "email" in adapter_config and "JIRA_EMAIL" not in env_vars:
|
|
209
244
|
env_vars["JIRA_EMAIL"] = adapter_config["email"]
|
|
210
245
|
|
|
211
246
|
if env_vars:
|
|
@@ -214,6 +249,108 @@ def create_mcp_server_config(
|
|
|
214
249
|
return config
|
|
215
250
|
|
|
216
251
|
|
|
252
|
+
def remove_claude_mcp(global_config: bool = False, dry_run: bool = False) -> None:
|
|
253
|
+
"""Remove mcp-ticketer from Claude Code/Desktop configuration.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
global_config: Remove from Claude Desktop instead of project-level
|
|
257
|
+
dry_run: Show what would be removed without making changes
|
|
258
|
+
|
|
259
|
+
"""
|
|
260
|
+
# Step 1: Find Claude MCP config location
|
|
261
|
+
config_type = "Claude Desktop" if global_config else "Claude Code"
|
|
262
|
+
console.print(f"[cyan]🔍 Removing {config_type} MCP configuration...[/cyan]")
|
|
263
|
+
|
|
264
|
+
mcp_config_path = find_claude_mcp_config(global_config)
|
|
265
|
+
console.print(f"[dim]Primary config: {mcp_config_path}[/dim]")
|
|
266
|
+
|
|
267
|
+
# Get absolute project path for Claude Code
|
|
268
|
+
absolute_project_path = str(Path.cwd().resolve()) if not global_config else None
|
|
269
|
+
|
|
270
|
+
# Step 2: Check if config file exists
|
|
271
|
+
if not mcp_config_path.exists():
|
|
272
|
+
console.print(f"[yellow]⚠ No configuration found at {mcp_config_path}[/yellow]")
|
|
273
|
+
console.print("[dim]mcp-ticketer is not configured for this platform[/dim]")
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
# Step 3: Load existing MCP configuration
|
|
277
|
+
is_claude_code = not global_config
|
|
278
|
+
mcp_config = load_claude_mcp_config(mcp_config_path, is_claude_code=is_claude_code)
|
|
279
|
+
|
|
280
|
+
# Step 4: Check if mcp-ticketer is configured
|
|
281
|
+
is_configured = False
|
|
282
|
+
if is_claude_code:
|
|
283
|
+
# Check Claude Code structure: .projects[path].mcpServers["mcp-ticketer"]
|
|
284
|
+
if absolute_project_path:
|
|
285
|
+
projects = mcp_config.get("projects", {})
|
|
286
|
+
project_config_entry = projects.get(absolute_project_path, {})
|
|
287
|
+
is_configured = "mcp-ticketer" in project_config_entry.get("mcpServers", {})
|
|
288
|
+
else:
|
|
289
|
+
# Check Claude Desktop structure: .mcpServers["mcp-ticketer"]
|
|
290
|
+
is_configured = "mcp-ticketer" in mcp_config.get("mcpServers", {})
|
|
291
|
+
|
|
292
|
+
if not is_configured:
|
|
293
|
+
console.print("[yellow]⚠ mcp-ticketer is not configured[/yellow]")
|
|
294
|
+
console.print(f"[dim]No mcp-ticketer entry found in {mcp_config_path}[/dim]")
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
# Step 5: Show what would be removed (dry run or actual removal)
|
|
298
|
+
if dry_run:
|
|
299
|
+
console.print("\n[cyan]DRY RUN - Would remove:[/cyan]")
|
|
300
|
+
console.print(" Server name: mcp-ticketer")
|
|
301
|
+
console.print(f" From: {mcp_config_path}")
|
|
302
|
+
if absolute_project_path:
|
|
303
|
+
console.print(f" Project: {absolute_project_path}")
|
|
304
|
+
return
|
|
305
|
+
|
|
306
|
+
# Step 6: Remove mcp-ticketer from configuration
|
|
307
|
+
if is_claude_code and absolute_project_path:
|
|
308
|
+
# Remove from Claude Code structure
|
|
309
|
+
del mcp_config["projects"][absolute_project_path]["mcpServers"]["mcp-ticketer"]
|
|
310
|
+
|
|
311
|
+
# Clean up empty structures
|
|
312
|
+
if not mcp_config["projects"][absolute_project_path]["mcpServers"]:
|
|
313
|
+
del mcp_config["projects"][absolute_project_path]["mcpServers"]
|
|
314
|
+
if not mcp_config["projects"][absolute_project_path]:
|
|
315
|
+
del mcp_config["projects"][absolute_project_path]
|
|
316
|
+
|
|
317
|
+
# Also remove from legacy location if it exists
|
|
318
|
+
legacy_config_path = Path.cwd() / ".claude" / "mcp.local.json"
|
|
319
|
+
if legacy_config_path.exists():
|
|
320
|
+
try:
|
|
321
|
+
legacy_config = load_claude_mcp_config(
|
|
322
|
+
legacy_config_path, is_claude_code=False
|
|
323
|
+
)
|
|
324
|
+
if "mcp-ticketer" in legacy_config.get("mcpServers", {}):
|
|
325
|
+
del legacy_config["mcpServers"]["mcp-ticketer"]
|
|
326
|
+
save_claude_mcp_config(legacy_config_path, legacy_config)
|
|
327
|
+
console.print("[dim]✓ Removed from legacy config as well[/dim]")
|
|
328
|
+
except Exception as e:
|
|
329
|
+
console.print(f"[dim]⚠ Could not remove from legacy config: {e}[/dim]")
|
|
330
|
+
else:
|
|
331
|
+
# Remove from Claude Desktop structure
|
|
332
|
+
del mcp_config["mcpServers"]["mcp-ticketer"]
|
|
333
|
+
|
|
334
|
+
# Step 7: Save updated configuration
|
|
335
|
+
try:
|
|
336
|
+
save_claude_mcp_config(mcp_config_path, mcp_config)
|
|
337
|
+
console.print("\n[green]✓ Successfully removed mcp-ticketer[/green]")
|
|
338
|
+
console.print(f"[dim]Configuration updated: {mcp_config_path}[/dim]")
|
|
339
|
+
|
|
340
|
+
# Next steps
|
|
341
|
+
console.print("\n[bold cyan]Next Steps:[/bold cyan]")
|
|
342
|
+
if global_config:
|
|
343
|
+
console.print("1. Restart Claude Desktop")
|
|
344
|
+
console.print("2. mcp-ticketer will no longer be available in MCP menu")
|
|
345
|
+
else:
|
|
346
|
+
console.print("1. Restart Claude Code")
|
|
347
|
+
console.print("2. mcp-ticketer will no longer be available in this project")
|
|
348
|
+
|
|
349
|
+
except Exception as e:
|
|
350
|
+
console.print(f"\n[red]✗ Failed to update configuration:[/red] {e}")
|
|
351
|
+
raise
|
|
352
|
+
|
|
353
|
+
|
|
217
354
|
def configure_claude_mcp(global_config: bool = False, force: bool = False) -> None:
|
|
218
355
|
"""Configure Claude Code to use mcp-ticketer.
|
|
219
356
|
|
|
@@ -222,18 +359,31 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
|
|
|
222
359
|
force: Overwrite existing configuration
|
|
223
360
|
|
|
224
361
|
Raises:
|
|
225
|
-
FileNotFoundError: If
|
|
362
|
+
FileNotFoundError: If Python executable or project config not found
|
|
226
363
|
ValueError: If configuration is invalid
|
|
227
364
|
|
|
228
365
|
"""
|
|
229
|
-
#
|
|
230
|
-
|
|
366
|
+
# Determine project path for venv detection
|
|
367
|
+
project_path = Path.cwd() if not global_config else None
|
|
368
|
+
|
|
369
|
+
# Step 1: Find Python executable (project-specific if available)
|
|
370
|
+
console.print("[cyan]🔍 Finding mcp-ticketer Python executable...[/cyan]")
|
|
231
371
|
try:
|
|
232
|
-
|
|
233
|
-
console.print(f"[green]✓[/green] Found: {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
372
|
+
python_path = get_mcp_ticketer_python(project_path=project_path)
|
|
373
|
+
console.print(f"[green]✓[/green] Found: {python_path}")
|
|
374
|
+
|
|
375
|
+
# Show if using project venv or fallback
|
|
376
|
+
if project_path and str(project_path / ".venv") in python_path:
|
|
377
|
+
console.print("[dim]Using project-specific venv[/dim]")
|
|
378
|
+
else:
|
|
379
|
+
console.print("[dim]Using pipx/system Python[/dim]")
|
|
380
|
+
except Exception as e:
|
|
381
|
+
console.print(f"[red]✗[/red] Could not find Python executable: {e}")
|
|
382
|
+
raise FileNotFoundError(
|
|
383
|
+
"Could not find mcp-ticketer Python executable. "
|
|
384
|
+
"Please ensure mcp-ticketer is installed.\n"
|
|
385
|
+
"Install with: pip install mcp-ticketer or pipx install mcp-ticketer"
|
|
386
|
+
) from e
|
|
237
387
|
|
|
238
388
|
# Step 2: Load project configuration
|
|
239
389
|
console.print("\n[cyan]📖 Reading project configuration...[/cyan]")
|
|
@@ -246,17 +396,34 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
|
|
|
246
396
|
raise
|
|
247
397
|
|
|
248
398
|
# Step 3: Find Claude MCP config location
|
|
249
|
-
config_type = "Claude Desktop" if global_config else "
|
|
399
|
+
config_type = "Claude Desktop" if global_config else "Claude Code"
|
|
250
400
|
console.print(f"\n[cyan]🔧 Configuring {config_type} MCP...[/cyan]")
|
|
251
401
|
|
|
252
402
|
mcp_config_path = find_claude_mcp_config(global_config)
|
|
253
|
-
console.print(f"[dim]
|
|
403
|
+
console.print(f"[dim]Primary config: {mcp_config_path}[/dim]")
|
|
404
|
+
|
|
405
|
+
# Get absolute project path for Claude Code
|
|
406
|
+
absolute_project_path = str(Path.cwd().resolve()) if not global_config else None
|
|
254
407
|
|
|
255
408
|
# Step 4: Load existing MCP configuration
|
|
256
|
-
|
|
409
|
+
is_claude_code = not global_config
|
|
410
|
+
mcp_config = load_claude_mcp_config(mcp_config_path, is_claude_code=is_claude_code)
|
|
257
411
|
|
|
258
412
|
# Step 5: Check if mcp-ticketer already configured
|
|
259
|
-
|
|
413
|
+
already_configured = False
|
|
414
|
+
if is_claude_code:
|
|
415
|
+
# Check Claude Code structure: .projects[path].mcpServers["mcp-ticketer"]
|
|
416
|
+
if absolute_project_path:
|
|
417
|
+
projects = mcp_config.get("projects", {})
|
|
418
|
+
project_config_entry = projects.get(absolute_project_path, {})
|
|
419
|
+
already_configured = "mcp-ticketer" in project_config_entry.get(
|
|
420
|
+
"mcpServers", {}
|
|
421
|
+
)
|
|
422
|
+
else:
|
|
423
|
+
# Check Claude Desktop structure: .mcpServers["mcp-ticketer"]
|
|
424
|
+
already_configured = "mcp-ticketer" in mcp_config.get("mcpServers", {})
|
|
425
|
+
|
|
426
|
+
if already_configured:
|
|
260
427
|
if not force:
|
|
261
428
|
console.print("[yellow]⚠ mcp-ticketer is already configured[/yellow]")
|
|
262
429
|
console.print("[dim]Use --force to overwrite existing configuration[/dim]")
|
|
@@ -265,16 +432,55 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
|
|
|
265
432
|
console.print("[yellow]⚠ Overwriting existing configuration[/yellow]")
|
|
266
433
|
|
|
267
434
|
# Step 6: Create mcp-ticketer server config
|
|
268
|
-
cwd = str(Path.cwd()) if not global_config else None
|
|
269
435
|
server_config = create_mcp_server_config(
|
|
270
|
-
|
|
436
|
+
python_path=python_path,
|
|
437
|
+
project_config=project_config,
|
|
438
|
+
project_path=absolute_project_path,
|
|
271
439
|
)
|
|
272
440
|
|
|
273
|
-
# Step 7: Update MCP configuration
|
|
274
|
-
if
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
441
|
+
# Step 7: Update MCP configuration based on platform
|
|
442
|
+
if is_claude_code:
|
|
443
|
+
# Claude Code: Write to ~/.claude.json with project-specific path
|
|
444
|
+
if absolute_project_path:
|
|
445
|
+
# Ensure projects structure exists
|
|
446
|
+
if "projects" not in mcp_config:
|
|
447
|
+
mcp_config["projects"] = {}
|
|
448
|
+
|
|
449
|
+
# Ensure project entry exists
|
|
450
|
+
if absolute_project_path not in mcp_config["projects"]:
|
|
451
|
+
mcp_config["projects"][absolute_project_path] = {}
|
|
452
|
+
|
|
453
|
+
# Ensure mcpServers for this project exists
|
|
454
|
+
if "mcpServers" not in mcp_config["projects"][absolute_project_path]:
|
|
455
|
+
mcp_config["projects"][absolute_project_path]["mcpServers"] = {}
|
|
456
|
+
|
|
457
|
+
# Add mcp-ticketer configuration
|
|
458
|
+
mcp_config["projects"][absolute_project_path]["mcpServers"][
|
|
459
|
+
"mcp-ticketer"
|
|
460
|
+
] = server_config
|
|
461
|
+
|
|
462
|
+
# Also write to backward-compatible location for older Claude Code versions
|
|
463
|
+
legacy_config_path = Path.cwd() / ".claude" / "mcp.local.json"
|
|
464
|
+
console.print(f"[dim]Legacy config: {legacy_config_path}[/dim]")
|
|
465
|
+
|
|
466
|
+
try:
|
|
467
|
+
legacy_config = load_claude_mcp_config(
|
|
468
|
+
legacy_config_path, is_claude_code=False
|
|
469
|
+
)
|
|
470
|
+
if "mcpServers" not in legacy_config:
|
|
471
|
+
legacy_config["mcpServers"] = {}
|
|
472
|
+
legacy_config["mcpServers"]["mcp-ticketer"] = server_config
|
|
473
|
+
save_claude_mcp_config(legacy_config_path, legacy_config)
|
|
474
|
+
console.print("[dim]✓ Backward-compatible config also written[/dim]")
|
|
475
|
+
except Exception as e:
|
|
476
|
+
console.print(
|
|
477
|
+
f"[dim]⚠ Could not write legacy config (non-fatal): {e}[/dim]"
|
|
478
|
+
)
|
|
479
|
+
else:
|
|
480
|
+
# Claude Desktop: Write to platform-specific config
|
|
481
|
+
if "mcpServers" not in mcp_config:
|
|
482
|
+
mcp_config["mcpServers"] = {}
|
|
483
|
+
mcp_config["mcpServers"]["mcp-ticketer"] = server_config
|
|
278
484
|
|
|
279
485
|
# Step 8: Save configuration
|
|
280
486
|
try:
|
|
@@ -286,9 +492,10 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
|
|
|
286
492
|
console.print("\n[bold]Configuration Details:[/bold]")
|
|
287
493
|
console.print(" Server name: mcp-ticketer")
|
|
288
494
|
console.print(f" Adapter: {adapter}")
|
|
289
|
-
console.print(f"
|
|
290
|
-
|
|
291
|
-
|
|
495
|
+
console.print(f" Python: {python_path}")
|
|
496
|
+
console.print(" Command: python -m mcp_ticketer.mcp.server")
|
|
497
|
+
if absolute_project_path:
|
|
498
|
+
console.print(f" Project path: {absolute_project_path}")
|
|
292
499
|
if "env" in server_config:
|
|
293
500
|
console.print(
|
|
294
501
|
f" Environment variables: {list(server_config['env'].keys())}"
|
|
@@ -22,14 +22,17 @@ def migrate_config_command(dry_run: bool = False) -> None:
|
|
|
22
22
|
"""
|
|
23
23
|
resolver = ConfigResolver()
|
|
24
24
|
|
|
25
|
+
# Get project config path (project-local only for security)
|
|
26
|
+
project_config_path = resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH
|
|
27
|
+
|
|
25
28
|
# Check if old config exists
|
|
26
|
-
if not
|
|
29
|
+
if not project_config_path.exists():
|
|
27
30
|
console.print("[yellow]No configuration found to migrate[/yellow]")
|
|
28
31
|
return
|
|
29
32
|
|
|
30
33
|
# Load old config
|
|
31
34
|
try:
|
|
32
|
-
with open(
|
|
35
|
+
with open(project_config_path) as f:
|
|
33
36
|
old_config = json.load(f)
|
|
34
37
|
except Exception as e:
|
|
35
38
|
console.print(f"[red]Failed to load config: {e}[/red]")
|
|
@@ -71,22 +74,23 @@ def migrate_config_command(dry_run: bool = False) -> None:
|
|
|
71
74
|
return
|
|
72
75
|
|
|
73
76
|
# Backup old config
|
|
74
|
-
|
|
77
|
+
project_config_path = resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH
|
|
78
|
+
backup_path = project_config_path.with_suffix(".json.bak")
|
|
75
79
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
76
|
-
backup_path =
|
|
80
|
+
backup_path = project_config_path.parent / f"config.{timestamp}.bak"
|
|
77
81
|
|
|
78
82
|
try:
|
|
79
|
-
shutil.copy(
|
|
83
|
+
shutil.copy(project_config_path, backup_path)
|
|
80
84
|
console.print(f"[green]✓[/green] Backed up old config to: {backup_path}")
|
|
81
85
|
except Exception as e:
|
|
82
86
|
console.print(f"[red]Failed to backup config: {e}[/red]")
|
|
83
87
|
return
|
|
84
88
|
|
|
85
|
-
# Save new config
|
|
89
|
+
# Save new config (to project-local config)
|
|
86
90
|
try:
|
|
87
|
-
resolver.
|
|
91
|
+
resolver.save_project_config(new_config)
|
|
88
92
|
console.print("[green]✓[/green] Migration complete!")
|
|
89
|
-
console.print(f"[dim]New config saved to: {
|
|
93
|
+
console.print(f"[dim]New config saved to: {project_config_path}[/dim]")
|
|
90
94
|
except Exception as e:
|
|
91
95
|
console.print(f"[red]Failed to save new config: {e}[/red]")
|
|
92
96
|
console.print(f"[yellow]Old config backed up at: {backup_path}[/yellow]")
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Platform-specific command groups."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
# Import platform-specific command modules
|
|
6
|
+
from .linear_commands import app as linear_app
|
|
7
|
+
|
|
8
|
+
# Create main platform command group
|
|
9
|
+
app = typer.Typer(
|
|
10
|
+
name="platform",
|
|
11
|
+
help="Platform-specific commands (Linear, JIRA, GitHub, AITrackdown)",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# Register Linear commands
|
|
15
|
+
app.add_typer(linear_app, name="linear")
|
|
16
|
+
|
|
17
|
+
# Create placeholder apps for other platforms
|
|
18
|
+
|
|
19
|
+
# JIRA platform commands (placeholder)
|
|
20
|
+
jira_app = typer.Typer(
|
|
21
|
+
name="jira",
|
|
22
|
+
help="JIRA-specific workspace and project management",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@jira_app.command("projects")
|
|
27
|
+
def jira_list_projects() -> None:
|
|
28
|
+
"""List JIRA projects (placeholder - not yet implemented)."""
|
|
29
|
+
from rich.console import Console
|
|
30
|
+
|
|
31
|
+
console = Console()
|
|
32
|
+
console.print("[yellow]JIRA platform commands are not yet implemented.[/yellow]")
|
|
33
|
+
console.print(
|
|
34
|
+
"Use the generic ticket commands for JIRA operations:\n"
|
|
35
|
+
" mcp-ticketer ticket create 'My ticket'\n"
|
|
36
|
+
" mcp-ticketer ticket list"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@jira_app.command("configure")
|
|
41
|
+
def jira_configure() -> None:
|
|
42
|
+
"""Configure JIRA adapter (placeholder - not yet implemented)."""
|
|
43
|
+
from rich.console import Console
|
|
44
|
+
|
|
45
|
+
console = Console()
|
|
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.")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# GitHub platform commands (placeholder)
|
|
51
|
+
github_app = typer.Typer(
|
|
52
|
+
name="github",
|
|
53
|
+
help="GitHub-specific repository and issue management",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@github_app.command("repos")
|
|
58
|
+
def github_list_repos() -> None:
|
|
59
|
+
"""List GitHub repositories (placeholder - not yet implemented)."""
|
|
60
|
+
from rich.console import Console
|
|
61
|
+
|
|
62
|
+
console = Console()
|
|
63
|
+
console.print("[yellow]GitHub platform commands are not yet implemented.[/yellow]")
|
|
64
|
+
console.print(
|
|
65
|
+
"Use the generic ticket commands for GitHub operations:\n"
|
|
66
|
+
" mcp-ticketer ticket create 'My issue'\n"
|
|
67
|
+
" mcp-ticketer ticket list"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@github_app.command("configure")
|
|
72
|
+
def github_configure() -> None:
|
|
73
|
+
"""Configure GitHub adapter (placeholder - not yet implemented)."""
|
|
74
|
+
from rich.console import Console
|
|
75
|
+
|
|
76
|
+
console = Console()
|
|
77
|
+
console.print("[yellow]GitHub platform commands are not yet implemented.[/yellow]")
|
|
78
|
+
console.print(
|
|
79
|
+
"Use 'mcp-ticketer init --adapter github' to configure GitHub adapter."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# AITrackdown platform commands (placeholder)
|
|
84
|
+
aitrackdown_app = typer.Typer(
|
|
85
|
+
name="aitrackdown",
|
|
86
|
+
help="AITrackdown-specific local file management",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@aitrackdown_app.command("info")
|
|
91
|
+
def aitrackdown_info() -> None:
|
|
92
|
+
"""Show AITrackdown storage information (placeholder - not yet implemented)."""
|
|
93
|
+
from rich.console import Console
|
|
94
|
+
|
|
95
|
+
console = Console()
|
|
96
|
+
console.print(
|
|
97
|
+
"[yellow]AITrackdown platform commands are not yet implemented.[/yellow]"
|
|
98
|
+
)
|
|
99
|
+
console.print(
|
|
100
|
+
"Use the generic ticket commands for AITrackdown operations:\n"
|
|
101
|
+
" mcp-ticketer ticket create 'My ticket'\n"
|
|
102
|
+
" mcp-ticketer ticket list"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@aitrackdown_app.command("configure")
|
|
107
|
+
def aitrackdown_configure() -> None:
|
|
108
|
+
"""Configure AITrackdown adapter (placeholder - not yet implemented)."""
|
|
109
|
+
from rich.console import Console
|
|
110
|
+
|
|
111
|
+
console = Console()
|
|
112
|
+
console.print(
|
|
113
|
+
"[yellow]AITrackdown platform commands are not yet implemented.[/yellow]"
|
|
114
|
+
)
|
|
115
|
+
console.print(
|
|
116
|
+
"Use 'mcp-ticketer init --adapter aitrackdown' to configure AITrackdown adapter."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# Register all platform command groups
|
|
121
|
+
app.add_typer(jira_app, name="jira")
|
|
122
|
+
app.add_typer(github_app, name="github")
|
|
123
|
+
app.add_typer(aitrackdown_app, name="aitrackdown")
|