mcp-ticketer 0.12.0__py3-none-any.whl → 2.2.13__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 (129) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/_version_scm.py +1 -0
  4. mcp_ticketer/adapters/aitrackdown.py +507 -6
  5. mcp_ticketer/adapters/asana/adapter.py +229 -0
  6. mcp_ticketer/adapters/asana/mappers.py +14 -0
  7. mcp_ticketer/adapters/github/__init__.py +26 -0
  8. mcp_ticketer/adapters/github/adapter.py +3229 -0
  9. mcp_ticketer/adapters/github/client.py +335 -0
  10. mcp_ticketer/adapters/github/mappers.py +797 -0
  11. mcp_ticketer/adapters/github/queries.py +692 -0
  12. mcp_ticketer/adapters/github/types.py +460 -0
  13. mcp_ticketer/adapters/hybrid.py +47 -5
  14. mcp_ticketer/adapters/jira/__init__.py +35 -0
  15. mcp_ticketer/adapters/jira/adapter.py +1351 -0
  16. mcp_ticketer/adapters/jira/client.py +271 -0
  17. mcp_ticketer/adapters/jira/mappers.py +246 -0
  18. mcp_ticketer/adapters/jira/queries.py +216 -0
  19. mcp_ticketer/adapters/jira/types.py +304 -0
  20. mcp_ticketer/adapters/linear/adapter.py +2730 -139
  21. mcp_ticketer/adapters/linear/client.py +175 -3
  22. mcp_ticketer/adapters/linear/mappers.py +203 -8
  23. mcp_ticketer/adapters/linear/queries.py +280 -3
  24. mcp_ticketer/adapters/linear/types.py +120 -4
  25. mcp_ticketer/analysis/__init__.py +56 -0
  26. mcp_ticketer/analysis/dependency_graph.py +255 -0
  27. mcp_ticketer/analysis/health_assessment.py +304 -0
  28. mcp_ticketer/analysis/orphaned.py +218 -0
  29. mcp_ticketer/analysis/project_status.py +594 -0
  30. mcp_ticketer/analysis/similarity.py +224 -0
  31. mcp_ticketer/analysis/staleness.py +266 -0
  32. mcp_ticketer/automation/__init__.py +11 -0
  33. mcp_ticketer/automation/project_updates.py +378 -0
  34. mcp_ticketer/cli/adapter_diagnostics.py +3 -1
  35. mcp_ticketer/cli/auggie_configure.py +17 -5
  36. mcp_ticketer/cli/codex_configure.py +97 -61
  37. mcp_ticketer/cli/configure.py +1288 -105
  38. mcp_ticketer/cli/cursor_configure.py +314 -0
  39. mcp_ticketer/cli/diagnostics.py +13 -12
  40. mcp_ticketer/cli/discover.py +5 -0
  41. mcp_ticketer/cli/gemini_configure.py +17 -5
  42. mcp_ticketer/cli/init_command.py +880 -0
  43. mcp_ticketer/cli/install_mcp_server.py +418 -0
  44. mcp_ticketer/cli/instruction_commands.py +6 -0
  45. mcp_ticketer/cli/main.py +267 -3175
  46. mcp_ticketer/cli/mcp_configure.py +821 -119
  47. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  48. mcp_ticketer/cli/platform_detection.py +77 -12
  49. mcp_ticketer/cli/platform_installer.py +545 -0
  50. mcp_ticketer/cli/project_update_commands.py +350 -0
  51. mcp_ticketer/cli/setup_command.py +795 -0
  52. mcp_ticketer/cli/simple_health.py +12 -10
  53. mcp_ticketer/cli/ticket_commands.py +705 -103
  54. mcp_ticketer/cli/utils.py +113 -0
  55. mcp_ticketer/core/__init__.py +56 -6
  56. mcp_ticketer/core/adapter.py +533 -2
  57. mcp_ticketer/core/config.py +21 -21
  58. mcp_ticketer/core/exceptions.py +7 -1
  59. mcp_ticketer/core/label_manager.py +732 -0
  60. mcp_ticketer/core/mappers.py +31 -19
  61. mcp_ticketer/core/milestone_manager.py +252 -0
  62. mcp_ticketer/core/models.py +480 -0
  63. mcp_ticketer/core/onepassword_secrets.py +1 -1
  64. mcp_ticketer/core/priority_matcher.py +463 -0
  65. mcp_ticketer/core/project_config.py +132 -14
  66. mcp_ticketer/core/project_utils.py +281 -0
  67. mcp_ticketer/core/project_validator.py +376 -0
  68. mcp_ticketer/core/session_state.py +176 -0
  69. mcp_ticketer/core/state_matcher.py +625 -0
  70. mcp_ticketer/core/url_parser.py +425 -0
  71. mcp_ticketer/core/validators.py +69 -0
  72. mcp_ticketer/mcp/server/__main__.py +2 -1
  73. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  74. mcp_ticketer/mcp/server/main.py +106 -25
  75. mcp_ticketer/mcp/server/routing.py +723 -0
  76. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  77. mcp_ticketer/mcp/server/tools/__init__.py +33 -11
  78. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  79. mcp_ticketer/mcp/server/tools/attachment_tools.py +5 -5
  80. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  81. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  82. mcp_ticketer/mcp/server/tools/config_tools.py +1391 -145
  83. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  84. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
  85. mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
  86. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  87. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  88. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  89. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  90. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  91. mcp_ticketer/mcp/server/tools/search_tools.py +209 -97
  92. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  93. mcp_ticketer/mcp/server/tools/ticket_tools.py +1107 -124
  94. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
  95. mcp_ticketer/queue/queue.py +68 -0
  96. mcp_ticketer/queue/worker.py +1 -1
  97. mcp_ticketer/utils/__init__.py +5 -0
  98. mcp_ticketer/utils/token_utils.py +246 -0
  99. mcp_ticketer-2.2.13.dist-info/METADATA +1396 -0
  100. mcp_ticketer-2.2.13.dist-info/RECORD +158 -0
  101. mcp_ticketer-2.2.13.dist-info/top_level.txt +2 -0
  102. py_mcp_installer/examples/phase3_demo.py +178 -0
  103. py_mcp_installer/scripts/manage_version.py +54 -0
  104. py_mcp_installer/setup.py +6 -0
  105. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  106. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  107. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  108. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  109. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  110. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  111. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  112. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  113. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  114. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  115. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  116. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  117. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  118. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  119. py_mcp_installer/tests/__init__.py +0 -0
  120. py_mcp_installer/tests/platforms/__init__.py +0 -0
  121. py_mcp_installer/tests/test_platform_detector.py +17 -0
  122. mcp_ticketer/adapters/github.py +0 -1574
  123. mcp_ticketer/adapters/jira.py +0 -1258
  124. mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
  125. mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
  126. mcp_ticketer-0.12.0.dist-info/top_level.txt +0 -1
  127. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
  128. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
  129. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,541 @@
1
+ """Configuration file manager with atomic operations and backup/restore.
2
+
3
+ This module provides a ConfigManager class that handles MCP configuration files
4
+ with atomic writes, automatic backups, and format validation for both JSON and TOML.
5
+
6
+ Design Philosophy:
7
+ - Atomic operations prevent partial writes
8
+ - Automatic backups before modifications
9
+ - Support both JSON (most platforms) and TOML (Codex)
10
+ - Graceful handling of missing files
11
+ - Legacy format migration support
12
+
13
+ Example:
14
+ >>> manager = ConfigManager(Path.home() / ".config/claude/mcp.json", ConfigFormat.JSON)
15
+ >>> config = manager.read()
16
+ >>> config["mcpServers"]["new-server"] = {"command": "test", "args": []}
17
+ >>> manager.write(config)
18
+ """
19
+
20
+ import json
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ # tomllib is imported conditionally in parse_toml_safe utility
25
+
26
+ try:
27
+ import tomli_w # For TOML writing # type: ignore[import-untyped]
28
+ except ImportError:
29
+ tomli_w = None # type: ignore[assignment,unused-ignore]
30
+
31
+ from .exceptions import BackupError, ConfigurationError, ValidationError
32
+ from .types import ConfigFormat, MCPServerConfig
33
+ from .utils import (
34
+ atomic_write,
35
+ backup_file,
36
+ parse_json_safe,
37
+ parse_toml_safe,
38
+ restore_backup,
39
+ )
40
+
41
+
42
+ class ConfigManager:
43
+ """Manage MCP configuration files with atomic operations.
44
+
45
+ Provides safe read/write operations for MCP configuration files with
46
+ automatic backup creation and validation. Supports both JSON and TOML formats.
47
+
48
+ Attributes:
49
+ config_path: Path to configuration file
50
+ format: Configuration file format (JSON or TOML)
51
+
52
+ Example:
53
+ >>> manager = ConfigManager(
54
+ ... Path.home() / ".config/claude/mcp.json",
55
+ ... ConfigFormat.JSON
56
+ ... )
57
+ >>> config = manager.read()
58
+ >>> manager.add_server(MCPServerConfig(
59
+ ... name="test",
60
+ ... command="test",
61
+ ... args=["run"]
62
+ ... ))
63
+ """
64
+
65
+ def __init__(self, config_path: Path, format: ConfigFormat) -> None:
66
+ """Initialize configuration manager.
67
+
68
+ Args:
69
+ config_path: Path to configuration file
70
+ format: Configuration file format (JSON or TOML)
71
+
72
+ Example:
73
+ >>> manager = ConfigManager(
74
+ ... Path(".claude.json"),
75
+ ... ConfigFormat.JSON
76
+ ... )
77
+ """
78
+ self.config_path = config_path
79
+ self.format = format
80
+
81
+ def read(self) -> dict[str, Any]:
82
+ """Read and parse configuration file.
83
+
84
+ Returns empty dict if file doesn't exist. Validates structure
85
+ and raises ConfigurationError if invalid.
86
+
87
+ Returns:
88
+ Configuration dictionary (empty dict if file missing)
89
+
90
+ Raises:
91
+ ConfigurationError: If file exists but is invalid
92
+
93
+ Example:
94
+ >>> manager = ConfigManager(Path(".claude.json"), ConfigFormat.JSON)
95
+ >>> config = manager.read()
96
+ >>> print(config.get("mcpServers", {}))
97
+ """
98
+ if self.format == ConfigFormat.JSON:
99
+ return parse_json_safe(self.config_path)
100
+ elif self.format == ConfigFormat.TOML:
101
+ return parse_toml_safe(self.config_path)
102
+ else:
103
+ raise ConfigurationError(
104
+ f"Unsupported config format: {self.format}",
105
+ config_path=str(self.config_path),
106
+ )
107
+
108
+ def write(self, config: dict[str, Any]) -> None:
109
+ """Write configuration with atomic operation.
110
+
111
+ Creates backup before writing. Uses atomic write pattern
112
+ (temp file + rename) to prevent partial writes.
113
+
114
+ Args:
115
+ config: Configuration dictionary to write
116
+
117
+ Raises:
118
+ BackupError: If backup creation fails
119
+ ConfigurationError: If write operation fails
120
+
121
+ Example:
122
+ >>> manager = ConfigManager(Path(".claude.json"), ConfigFormat.JSON)
123
+ >>> config = {"mcpServers": {"test": {"command": "test"}}}
124
+ >>> manager.write(config)
125
+ """
126
+ # Create backup if file exists
127
+ if self.config_path.exists():
128
+ try:
129
+ backup_file(self.config_path)
130
+ except Exception as e:
131
+ raise BackupError(f"Failed to backup before write: {e}") from e
132
+
133
+ # Serialize configuration
134
+ try:
135
+ if self.format == ConfigFormat.JSON:
136
+ content = json.dumps(config, indent=2) + "\n"
137
+ elif self.format == ConfigFormat.TOML:
138
+ if tomli_w is None:
139
+ raise ConfigurationError(
140
+ "TOML write support requires tomli-w package",
141
+ config_path=str(self.config_path),
142
+ )
143
+ import io
144
+
145
+ # tomli_w.dump requires binary mode IO
146
+ buffer = io.BytesIO()
147
+ tomli_w.dump(config, buffer)
148
+ content = buffer.getvalue().decode("utf-8")
149
+ else:
150
+ raise ConfigurationError(
151
+ f"Unsupported config format: {self.format}",
152
+ config_path=str(self.config_path),
153
+ )
154
+ except Exception as e:
155
+ raise ConfigurationError(
156
+ f"Failed to serialize config: {e}", config_path=str(self.config_path)
157
+ ) from e
158
+
159
+ # Write atomically
160
+ try:
161
+ atomic_write(self.config_path, content)
162
+ except Exception as e:
163
+ raise ConfigurationError(
164
+ f"Failed to write config: {e}", config_path=str(self.config_path)
165
+ ) from e
166
+
167
+ def backup(self) -> Path:
168
+ """Create timestamped backup of current config.
169
+
170
+ Backups are stored in .mcp-installer-backups/ directory with
171
+ timestamp in filename.
172
+
173
+ Returns:
174
+ Path to created backup file
175
+
176
+ Raises:
177
+ BackupError: If backup creation fails
178
+
179
+ Example:
180
+ >>> manager = ConfigManager(Path(".claude.json"), ConfigFormat.JSON)
181
+ >>> backup_path = manager.backup()
182
+ >>> print(backup_path)
183
+ .mcp-installer-backups/.claude.json.20250105_143022.backup
184
+ """
185
+ if not self.config_path.exists():
186
+ raise BackupError(f"Cannot backup non-existent file: {self.config_path}")
187
+
188
+ return backup_file(self.config_path)
189
+
190
+ def restore(self, backup_path: Path) -> None:
191
+ """Restore configuration from backup file.
192
+
193
+ Args:
194
+ backup_path: Path to backup file to restore
195
+
196
+ Raises:
197
+ BackupError: If restore fails
198
+
199
+ Example:
200
+ >>> manager = ConfigManager(Path(".claude.json"), ConfigFormat.JSON)
201
+ >>> backup_path = manager.backup()
202
+ >>> # ... make changes ...
203
+ >>> manager.restore(backup_path) # Rollback changes
204
+ """
205
+ restore_backup(backup_path, self.config_path)
206
+
207
+ def add_server(self, server: MCPServerConfig) -> None:
208
+ """Add MCP server to configuration.
209
+
210
+ Reads current config, adds server, and writes atomically.
211
+ Creates backup before modification.
212
+
213
+ Args:
214
+ server: Server configuration to add
215
+
216
+ Raises:
217
+ ValidationError: If server with same name already exists
218
+ ConfigurationError: If write fails
219
+
220
+ Example:
221
+ >>> manager = ConfigManager(Path(".claude.json"), ConfigFormat.JSON)
222
+ >>> server = MCPServerConfig(
223
+ ... name="mcp-ticketer",
224
+ ... command="uv",
225
+ ... args=["run", "mcp-ticketer", "mcp"],
226
+ ... env={"API_KEY": "..."}
227
+ ... )
228
+ >>> manager.add_server(server)
229
+ """
230
+ config = self.read()
231
+
232
+ # Determine servers key based on format
233
+ servers_key = (
234
+ "mcpServers" if self.format == ConfigFormat.JSON else "mcp_servers"
235
+ )
236
+
237
+ # Initialize servers section if missing
238
+ if servers_key not in config:
239
+ config[servers_key] = {}
240
+
241
+ # Check if server already exists
242
+ if server.name in config[servers_key]:
243
+ raise ValidationError(
244
+ f"Server '{server.name}' already exists in configuration",
245
+ recovery_suggestion=(
246
+ "Use update_server() to modify existing server, "
247
+ "or remove it first"
248
+ ),
249
+ )
250
+
251
+ # Build server config dict
252
+ server_dict: dict[str, Any] = {
253
+ "command": server.command,
254
+ "args": list(server.args), # Convert to list to ensure JSON serialization
255
+ }
256
+
257
+ # Add optional fields
258
+ if server.env:
259
+ server_dict["env"] = dict(server.env) # Convert to dict
260
+ if server.description:
261
+ server_dict["description"] = server.description
262
+
263
+ # Add server
264
+ config[servers_key][server.name] = server_dict
265
+
266
+ # Write config
267
+ self.write(config)
268
+
269
+ def remove_server(self, name: str) -> None:
270
+ """Remove MCP server from configuration.
271
+
272
+ Args:
273
+ name: Name of server to remove
274
+
275
+ Raises:
276
+ ValidationError: If server doesn't exist
277
+ ConfigurationError: If write fails
278
+
279
+ Example:
280
+ >>> manager = ConfigManager(Path(".claude.json"), ConfigFormat.JSON)
281
+ >>> manager.remove_server("mcp-ticketer")
282
+ """
283
+ config = self.read()
284
+
285
+ # Determine servers key
286
+ servers_key = (
287
+ "mcpServers" if self.format == ConfigFormat.JSON else "mcp_servers"
288
+ )
289
+
290
+ # Check if server exists
291
+ if servers_key not in config or name not in config[servers_key]:
292
+ raise ValidationError(
293
+ f"Server '{name}' not found in configuration",
294
+ recovery_suggestion="Use list_servers() to see available servers",
295
+ )
296
+
297
+ # Remove server
298
+ del config[servers_key][name]
299
+
300
+ # Write config
301
+ self.write(config)
302
+
303
+ def update_server(self, name: str, server: MCPServerConfig) -> None:
304
+ """Update existing server configuration.
305
+
306
+ Args:
307
+ name: Name of server to update
308
+ server: New server configuration
309
+
310
+ Raises:
311
+ ValidationError: If server doesn't exist
312
+ ConfigurationError: If write fails
313
+
314
+ Example:
315
+ >>> manager = ConfigManager(Path(".claude.json"), ConfigFormat.JSON)
316
+ >>> updated = MCPServerConfig(
317
+ ... name="mcp-ticketer",
318
+ ... command="mcp-ticketer", # Changed from uv run
319
+ ... args=["mcp"]
320
+ ... )
321
+ >>> manager.update_server("mcp-ticketer", updated)
322
+ """
323
+ config = self.read()
324
+
325
+ # Determine servers key
326
+ servers_key = (
327
+ "mcpServers" if self.format == ConfigFormat.JSON else "mcp_servers"
328
+ )
329
+
330
+ # Check if server exists
331
+ if servers_key not in config or name not in config[servers_key]:
332
+ raise ValidationError(
333
+ f"Server '{name}' not found in configuration",
334
+ recovery_suggestion="Use add_server() to create new server",
335
+ )
336
+
337
+ # Build updated server config
338
+ server_dict: dict[str, Any] = {
339
+ "command": server.command,
340
+ "args": list(server.args),
341
+ }
342
+
343
+ if server.env:
344
+ server_dict["env"] = dict(server.env)
345
+ if server.description:
346
+ server_dict["description"] = server.description
347
+
348
+ # Update server
349
+ config[servers_key][name] = server_dict
350
+
351
+ # Write config
352
+ self.write(config)
353
+
354
+ def list_servers(self) -> list[MCPServerConfig]:
355
+ """List all configured MCP servers.
356
+
357
+ Returns:
358
+ List of server configurations
359
+
360
+ Example:
361
+ >>> manager = ConfigManager(Path(".claude.json"), ConfigFormat.JSON)
362
+ >>> servers = manager.list_servers()
363
+ >>> for server in servers:
364
+ ... print(f"{server.name}: {server.command}")
365
+ """
366
+ config = self.read()
367
+
368
+ # Determine servers key
369
+ servers_key = (
370
+ "mcpServers" if self.format == ConfigFormat.JSON else "mcp_servers"
371
+ )
372
+
373
+ servers: list[MCPServerConfig] = []
374
+
375
+ for name, server_dict in config.get(servers_key, {}).items():
376
+ if not isinstance(server_dict, dict):
377
+ continue # Skip invalid entries
378
+
379
+ servers.append(
380
+ MCPServerConfig(
381
+ name=name,
382
+ command=server_dict.get("command", ""),
383
+ args=server_dict.get("args", []),
384
+ env=server_dict.get("env", {}),
385
+ description=server_dict.get("description", ""),
386
+ )
387
+ )
388
+
389
+ return servers
390
+
391
+ def get_server(self, name: str) -> MCPServerConfig | None:
392
+ """Get specific server configuration.
393
+
394
+ Args:
395
+ name: Server name to lookup
396
+
397
+ Returns:
398
+ Server configuration if found, None otherwise
399
+
400
+ Example:
401
+ >>> manager = ConfigManager(Path(".claude.json"), ConfigFormat.JSON)
402
+ >>> server = manager.get_server("mcp-ticketer")
403
+ >>> if server:
404
+ ... print(f"Command: {server.command}")
405
+ """
406
+ config = self.read()
407
+
408
+ # Determine servers key
409
+ servers_key = (
410
+ "mcpServers" if self.format == ConfigFormat.JSON else "mcp_servers"
411
+ )
412
+
413
+ server_dict = config.get(servers_key, {}).get(name)
414
+ if not server_dict or not isinstance(server_dict, dict):
415
+ return None
416
+
417
+ return MCPServerConfig(
418
+ name=name,
419
+ command=server_dict.get("command", ""),
420
+ args=server_dict.get("args", []),
421
+ env=server_dict.get("env", {}),
422
+ description=server_dict.get("description", ""),
423
+ )
424
+
425
+ def validate(self) -> list[str]:
426
+ """Validate configuration structure.
427
+
428
+ Returns list of validation issues found. Empty list means valid.
429
+
430
+ Returns:
431
+ List of validation error messages (empty if valid)
432
+
433
+ Example:
434
+ >>> manager = ConfigManager(Path(".claude.json"), ConfigFormat.JSON)
435
+ >>> issues = manager.validate()
436
+ >>> if issues:
437
+ ... print("Configuration issues:")
438
+ ... for issue in issues:
439
+ ... print(f" - {issue}")
440
+ """
441
+ issues: list[str] = []
442
+
443
+ try:
444
+ config = self.read()
445
+ except ConfigurationError as e:
446
+ return [f"Failed to read config: {e.message}"]
447
+
448
+ # Determine servers key
449
+ servers_key = (
450
+ "mcpServers" if self.format == ConfigFormat.JSON else "mcp_servers"
451
+ )
452
+
453
+ # Check for servers key
454
+ if servers_key not in config:
455
+ issues.append(f"Missing '{servers_key}' key in configuration")
456
+ return issues
457
+
458
+ servers = config[servers_key]
459
+ if not isinstance(servers, dict):
460
+ issues.append(f"'{servers_key}' must be a dictionary/table")
461
+ return issues
462
+
463
+ # Validate each server
464
+ for server_name, server_config in servers.items():
465
+ if not isinstance(server_config, dict):
466
+ issues.append(f"Server '{server_name}' config must be a dictionary")
467
+ continue
468
+
469
+ # Check required fields
470
+ if "command" not in server_config:
471
+ issues.append(
472
+ f"Server '{server_name}' missing required 'command' field"
473
+ )
474
+
475
+ # Validate field types
476
+ if "args" in server_config and not isinstance(server_config["args"], list):
477
+ issues.append(f"Server '{server_name}' 'args' must be a list")
478
+
479
+ if "env" in server_config and not isinstance(server_config["env"], dict):
480
+ issues.append(f"Server '{server_name}' 'env' must be a dictionary")
481
+
482
+ return issues
483
+
484
+ def migrate_legacy(self) -> bool:
485
+ """Detect and migrate legacy line-delimited JSON format.
486
+
487
+ Checks if any servers use deprecated python module format and
488
+ migrates them to modern format.
489
+
490
+ Returns:
491
+ True if migration was performed, False if not needed
492
+
493
+ Example:
494
+ >>> manager = ConfigManager(Path(".claude.json"), ConfigFormat.JSON)
495
+ >>> if manager.migrate_legacy():
496
+ ... print("Legacy servers migrated successfully")
497
+ """
498
+ # Only applies to JSON format
499
+ if self.format != ConfigFormat.JSON:
500
+ return False
501
+
502
+ config = self.read()
503
+ servers = config.get("mcpServers", {})
504
+
505
+ migrated = False
506
+
507
+ for server_name, server_config in servers.items():
508
+ if not isinstance(server_config, dict):
509
+ continue
510
+
511
+ args = server_config.get("args", [])
512
+
513
+ # Check for legacy python module format
514
+ # Old: ["python", "-m", "mcp_ticketer.mcp.server"]
515
+ # New: ["uv", "run", "mcp-ticketer", "mcp"]
516
+ if (
517
+ len(args) >= 2
518
+ and args[0] == "-m"
519
+ and "mcp_ticketer.mcp.server" in args[1]
520
+ ):
521
+ # Migrate to modern format
522
+ # Try to detect best command (uv, pipx, or binary)
523
+ from .utils import resolve_command_path
524
+
525
+ if resolve_command_path("uv"):
526
+ server_config["command"] = "uv"
527
+ server_config["args"] = ["run", "mcp-ticketer", "mcp"]
528
+ elif resolve_command_path("mcp-ticketer"):
529
+ server_config["command"] = str(resolve_command_path("mcp-ticketer"))
530
+ server_config["args"] = ["mcp"]
531
+ else:
532
+ # Keep python fallback but use modern entry point
533
+ # Leave command as-is (should be python path)
534
+ server_config["args"] = ["-m", "mcp_ticketer.mcp.server"]
535
+
536
+ migrated = True
537
+
538
+ if migrated:
539
+ self.write(config)
540
+
541
+ return migrated