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,750 @@
1
+ """MCP Inspector for validation and health checking.
2
+
3
+ This module provides comprehensive validation and inspection of MCP server
4
+ installations, including legacy format detection, command verification, and
5
+ auto-fix capabilities.
6
+
7
+ Design Philosophy:
8
+ - Comprehensive validation of server configurations
9
+ - Detect legacy formats and migration needs
10
+ - Auto-fix common issues where possible
11
+ - Clear severity levels (error, warning, info)
12
+ - Actionable recommendations
13
+
14
+ Example:
15
+ >>> from py_mcp_installer import MCPInspector, PlatformDetector
16
+ >>> detector = PlatformDetector()
17
+ >>> info = detector.detect()
18
+ >>> inspector = MCPInspector(info)
19
+ >>> report = inspector.inspect()
20
+ >>> print(f"Found {len(report.issues)} issues")
21
+ >>> for issue in report.issues:
22
+ ... print(f"{issue.severity}: {issue.message}")
23
+ """
24
+
25
+ import json
26
+ import logging
27
+ from dataclasses import dataclass, field
28
+ from pathlib import Path
29
+ from typing import Any, Literal
30
+
31
+ from .config_manager import ConfigManager
32
+ from .exceptions import ConfigurationError
33
+ from .types import ConfigFormat, MCPServerConfig, Platform, PlatformInfo
34
+ from .utils import resolve_command_path
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+ # ============================================================================
39
+ # Data Classes
40
+ # ============================================================================
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class ValidationIssue:
45
+ """Represents a validation issue found in configuration.
46
+
47
+ Attributes:
48
+ severity: Issue severity level
49
+ - error: Prevents server from working
50
+ - warning: May cause problems
51
+ - info: Recommendations only
52
+ message: Human-readable issue description
53
+ server_name: Affected server name (None for global issues)
54
+ fix_suggestion: How to fix this issue
55
+ auto_fixable: Whether this can be auto-fixed
56
+
57
+ Example:
58
+ >>> issue = ValidationIssue(
59
+ ... severity="error",
60
+ ... message="Command 'mcp-ticketer' not found in PATH",
61
+ ... server_name="mcp-ticketer",
62
+ ... fix_suggestion="Install with: pipx install mcp-ticketer",
63
+ ... auto_fixable=False
64
+ ... )
65
+ """
66
+
67
+ severity: Literal["error", "warning", "info"]
68
+ message: str
69
+ server_name: str | None
70
+ fix_suggestion: str
71
+ auto_fixable: bool = False
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class InspectionReport:
76
+ """Complete inspection report.
77
+
78
+ Attributes:
79
+ platform: Detected platform
80
+ config_path: Path to configuration file
81
+ total_servers: Total number of servers found
82
+ valid_servers: Number of valid servers
83
+ issues: List of validation issues
84
+ recommendations: General recommendations for improvement
85
+
86
+ Example:
87
+ >>> report = InspectionReport(
88
+ ... platform=Platform.CLAUDE_CODE,
89
+ ... config_path=Path.home() / ".config/claude/mcp.json",
90
+ ... total_servers=5,
91
+ ... valid_servers=4,
92
+ ... issues=[...],
93
+ ... recommendations=["Migrate to uv run for faster startup"]
94
+ ... )
95
+ """
96
+
97
+ platform: Platform
98
+ config_path: Path
99
+ total_servers: int
100
+ valid_servers: int
101
+ issues: list[ValidationIssue] = field(default_factory=list)
102
+ recommendations: list[str] = field(default_factory=list)
103
+
104
+ def has_errors(self) -> bool:
105
+ """Check if report contains any errors.
106
+
107
+ Returns:
108
+ True if any error-level issues found
109
+ """
110
+ return any(issue.severity == "error" for issue in self.issues)
111
+
112
+ def has_warnings(self) -> bool:
113
+ """Check if report contains any warnings.
114
+
115
+ Returns:
116
+ True if any warning-level issues found
117
+ """
118
+ return any(issue.severity == "warning" for issue in self.issues)
119
+
120
+ def summary(self) -> str:
121
+ """Generate human-readable summary.
122
+
123
+ Returns:
124
+ Summary string with counts and status
125
+ """
126
+ errors = sum(1 for i in self.issues if i.severity == "error")
127
+ warnings = sum(1 for i in self.issues if i.severity == "warning")
128
+ infos = sum(1 for i in self.issues if i.severity == "info")
129
+
130
+ status = "PASS" if errors == 0 else "FAIL"
131
+ return (
132
+ f"Inspection {status}: {self.valid_servers}/{self.total_servers} servers valid\n"
133
+ f" Errors: {errors}, Warnings: {warnings}, Info: {infos}"
134
+ )
135
+
136
+
137
+ # ============================================================================
138
+ # MCP Inspector
139
+ # ============================================================================
140
+
141
+
142
+ class MCPInspector:
143
+ """Inspect and validate MCP server installations.
144
+
145
+ Provides comprehensive validation of MCP configurations including:
146
+ - Command existence and accessibility
147
+ - Configuration file validity
148
+ - Legacy format detection
149
+ - Duplicate server detection
150
+ - Auto-fix capabilities for common issues
151
+
152
+ Example:
153
+ >>> from py_mcp_installer import PlatformDetector
154
+ >>> detector = PlatformDetector()
155
+ >>> info = detector.detect()
156
+ >>> inspector = MCPInspector(info)
157
+ >>> report = inspector.inspect()
158
+ >>> if report.has_errors():
159
+ ... print("Found errors:", report.summary())
160
+ """
161
+
162
+ def __init__(self, platform_info: PlatformInfo) -> None:
163
+ """Initialize inspector with detected platform info.
164
+
165
+ Args:
166
+ platform_info: Platform information from PlatformDetector
167
+
168
+ Example:
169
+ >>> from py_mcp_installer import PlatformDetector
170
+ >>> detector = PlatformDetector()
171
+ >>> info = detector.detect()
172
+ >>> inspector = MCPInspector(info)
173
+ """
174
+ self.platform_info = platform_info
175
+ self.config_path = platform_info.config_path or Path()
176
+
177
+ # Determine config format based on platform
178
+ if platform_info.platform == Platform.CODEX:
179
+ self.config_format = ConfigFormat.TOML
180
+ else:
181
+ self.config_format = ConfigFormat.JSON
182
+
183
+ self.config_manager = ConfigManager(self.config_path, self.config_format)
184
+
185
+ def inspect(self) -> InspectionReport:
186
+ """Run complete inspection and return report.
187
+
188
+ Performs all validation checks including:
189
+ - Config file existence and validity
190
+ - Server configuration validation
191
+ - Legacy format detection
192
+ - Duplicate detection
193
+ - Command availability checks
194
+
195
+ Returns:
196
+ Complete inspection report with issues and recommendations
197
+
198
+ Example:
199
+ >>> inspector = MCPInspector(platform_info)
200
+ >>> report = inspector.inspect()
201
+ >>> print(report.summary())
202
+ >>> for issue in report.issues:
203
+ ... if issue.severity == "error":
204
+ ... print(f"ERROR: {issue.message}")
205
+ """
206
+ issues: list[ValidationIssue] = []
207
+ recommendations: list[str] = []
208
+
209
+ # Check if config file exists
210
+ if not self.config_path.exists():
211
+ issues.append(
212
+ ValidationIssue(
213
+ severity="warning",
214
+ message=f"Configuration file not found: {self.config_path}",
215
+ server_name=None,
216
+ fix_suggestion=(
217
+ "Create config file or run installer to initialize"
218
+ ),
219
+ auto_fixable=True,
220
+ )
221
+ )
222
+ return InspectionReport(
223
+ platform=self.platform_info.platform,
224
+ config_path=self.config_path,
225
+ total_servers=0,
226
+ valid_servers=0,
227
+ issues=issues,
228
+ recommendations=recommendations,
229
+ )
230
+
231
+ # Check for legacy format
232
+ if self.check_legacy_format():
233
+ issues.append(
234
+ ValidationIssue(
235
+ severity="warning",
236
+ message="Legacy line-delimited JSON format detected",
237
+ server_name=None,
238
+ fix_suggestion="Run migration to convert to FastMCP SDK format",
239
+ auto_fixable=True,
240
+ )
241
+ )
242
+ recommendations.extend(self.suggest_migration())
243
+
244
+ # Read and validate config
245
+ try:
246
+ config = self.config_manager.read()
247
+ except ConfigurationError as e:
248
+ issues.append(
249
+ ValidationIssue(
250
+ severity="error",
251
+ message=f"Failed to parse config: {e.message}",
252
+ server_name=None,
253
+ fix_suggestion=e.recovery_suggestion,
254
+ auto_fixable=False,
255
+ )
256
+ )
257
+ return InspectionReport(
258
+ platform=self.platform_info.platform,
259
+ config_path=self.config_path,
260
+ total_servers=0,
261
+ valid_servers=0,
262
+ issues=issues,
263
+ recommendations=recommendations,
264
+ )
265
+
266
+ # Get servers from config
267
+ servers = self._extract_servers(config)
268
+ total_servers = len(servers)
269
+ valid_servers = 0
270
+
271
+ # Validate each server
272
+ for server in servers:
273
+ server_issues = self.validate_server(server)
274
+ if not any(issue.severity == "error" for issue in server_issues):
275
+ valid_servers += 1
276
+ issues.extend(server_issues)
277
+
278
+ # Check for duplicates
279
+ duplicates = self.find_duplicates(config)
280
+ for name1, name2 in duplicates:
281
+ issues.append(
282
+ ValidationIssue(
283
+ severity="warning",
284
+ message=f"Duplicate server names detected: {name1}, {name2}",
285
+ server_name=None,
286
+ fix_suggestion="Rename one of the servers to avoid conflicts",
287
+ auto_fixable=False,
288
+ )
289
+ )
290
+
291
+ # Add general recommendations
292
+ recommendations.extend(self._generate_recommendations(servers))
293
+
294
+ return InspectionReport(
295
+ platform=self.platform_info.platform,
296
+ config_path=self.config_path,
297
+ total_servers=total_servers,
298
+ valid_servers=valid_servers,
299
+ issues=issues,
300
+ recommendations=recommendations,
301
+ )
302
+
303
+ def validate_server(self, server: MCPServerConfig) -> list[ValidationIssue]:
304
+ """Validate individual server configuration.
305
+
306
+ Checks:
307
+ - Command exists and is executable
308
+ - Required fields are present
309
+ - Environment variables are set (warnings only)
310
+ - Arguments are valid
311
+
312
+ Args:
313
+ server: Server configuration to validate
314
+
315
+ Returns:
316
+ List of validation issues (empty if valid)
317
+
318
+ Example:
319
+ >>> server = MCPServerConfig(
320
+ ... name="test",
321
+ ... command="nonexistent",
322
+ ... args=[]
323
+ ... )
324
+ >>> issues = inspector.validate_server(server)
325
+ >>> if issues:
326
+ ... print(f"Server invalid: {issues[0].message}")
327
+ """
328
+ issues: list[ValidationIssue] = []
329
+
330
+ # Check required fields
331
+ if not server.name:
332
+ issues.append(
333
+ ValidationIssue(
334
+ severity="error",
335
+ message="Server missing required 'name' field",
336
+ server_name=None,
337
+ fix_suggestion="Add server name",
338
+ auto_fixable=False,
339
+ )
340
+ )
341
+
342
+ if not server.command:
343
+ issues.append(
344
+ ValidationIssue(
345
+ severity="error",
346
+ message=f"Server '{server.name}' missing required 'command' field",
347
+ server_name=server.name,
348
+ fix_suggestion="Add command field",
349
+ auto_fixable=False,
350
+ )
351
+ )
352
+ return issues # Can't continue without command
353
+
354
+ # Check if command exists
355
+ if not self.check_command_exists(server.command):
356
+ issues.append(
357
+ ValidationIssue(
358
+ severity="error",
359
+ message=f"Command not found: {server.command}",
360
+ server_name=server.name,
361
+ fix_suggestion=self._suggest_command_install(server.command),
362
+ auto_fixable=False,
363
+ )
364
+ )
365
+
366
+ # Check for missing description (info only)
367
+ if not server.description:
368
+ issues.append(
369
+ ValidationIssue(
370
+ severity="info",
371
+ message=f"Server '{server.name}' missing description",
372
+ server_name=server.name,
373
+ fix_suggestion="Add description for better documentation",
374
+ auto_fixable=False,
375
+ )
376
+ )
377
+
378
+ # Check for environment variables that look like placeholders
379
+ for key, value in server.env.items():
380
+ if value.startswith("<") and value.endswith(">"):
381
+ issues.append(
382
+ ValidationIssue(
383
+ severity="warning",
384
+ message=(
385
+ f"Server '{server.name}' has placeholder env var: "
386
+ f"{key}={value}"
387
+ ),
388
+ server_name=server.name,
389
+ fix_suggestion=f"Set actual value for {key}",
390
+ auto_fixable=False,
391
+ )
392
+ )
393
+
394
+ # Check for deprecated args patterns
395
+ deprecated_patterns = {
396
+ "--legacy-mode": "Use new format without --legacy-mode",
397
+ "--old-api": "Update to new API",
398
+ }
399
+ for arg in server.args:
400
+ for pattern, suggestion in deprecated_patterns.items():
401
+ if pattern in arg:
402
+ issues.append(
403
+ ValidationIssue(
404
+ severity="warning",
405
+ message=f"Server '{server.name}' uses deprecated arg: {arg}",
406
+ server_name=server.name,
407
+ fix_suggestion=suggestion,
408
+ auto_fixable=True,
409
+ )
410
+ )
411
+
412
+ return issues
413
+
414
+ def check_command_exists(self, command: str) -> bool:
415
+ """Check if command is executable.
416
+
417
+ Args:
418
+ command: Command to check (e.g., "uv", "/usr/bin/python")
419
+
420
+ Returns:
421
+ True if command exists and is executable
422
+
423
+ Example:
424
+ >>> inspector.check_command_exists("python")
425
+ True
426
+ >>> inspector.check_command_exists("nonexistent-command")
427
+ False
428
+ """
429
+ # Try to resolve command path
430
+ resolved = resolve_command_path(command)
431
+ return resolved is not None
432
+
433
+ def check_legacy_format(self) -> bool:
434
+ """Detect line-delimited JSON format (pre-FastMCP SDK).
435
+
436
+ The legacy format used line-delimited JSON objects instead of
437
+ a single JSON object with mcpServers key.
438
+
439
+ Returns:
440
+ True if legacy format detected
441
+
442
+ Example:
443
+ >>> if inspector.check_legacy_format():
444
+ ... print("Need to migrate to new format")
445
+ """
446
+ if not self.config_path.exists():
447
+ return False
448
+
449
+ if self.config_format != ConfigFormat.JSON:
450
+ return False # Only JSON configs can be legacy format
451
+
452
+ try:
453
+ content = self.config_path.read_text(encoding="utf-8")
454
+
455
+ # Legacy format has multiple JSON objects separated by newlines
456
+ # Modern format has single JSON object
457
+ lines = [line.strip() for line in content.split("\n") if line.strip()]
458
+
459
+ if len(lines) <= 1:
460
+ return False # Single object, not legacy
461
+
462
+ # Try to parse as line-delimited JSON
463
+ for line in lines:
464
+ try:
465
+ json.loads(line)
466
+ except json.JSONDecodeError:
467
+ return False # Not valid line-delimited JSON
468
+
469
+ return True # All lines are valid JSON = legacy format
470
+
471
+ except Exception as e:
472
+ logger.warning(f"Error checking legacy format: {e}")
473
+ return False
474
+
475
+ def suggest_migration(self) -> list[str]:
476
+ """Suggest migration steps for legacy format.
477
+
478
+ Returns:
479
+ List of migration steps
480
+
481
+ Example:
482
+ >>> steps = inspector.suggest_migration()
483
+ >>> for step in steps:
484
+ ... print(f"- {step}")
485
+ """
486
+ return [
487
+ "Backup current config before migration",
488
+ "Run migration to convert line-delimited JSON to modern format",
489
+ "Validate new config with inspector",
490
+ "Restart platform to pick up new config",
491
+ ]
492
+
493
+ def find_duplicates(self, config: dict[str, Any]) -> list[tuple[str, str]]:
494
+ """Find duplicate server names or commands.
495
+
496
+ Args:
497
+ config: Configuration dictionary
498
+
499
+ Returns:
500
+ List of (name1, name2) tuples for duplicates
501
+
502
+ Example:
503
+ >>> duplicates = inspector.find_duplicates(config)
504
+ >>> if duplicates:
505
+ ... print(f"Found {len(duplicates)} duplicate pairs")
506
+ """
507
+ duplicates: list[tuple[str, str]] = []
508
+ servers = self._extract_servers(config)
509
+
510
+ # Check for duplicate names
511
+ names = [s.name for s in servers]
512
+ seen_names: set[str] = set()
513
+ for name in names:
514
+ if name in seen_names:
515
+ # Find other server with same name
516
+ for other in servers:
517
+ if other.name == name and other.name not in [
518
+ d[0] for d in duplicates
519
+ ]:
520
+ duplicates.append((name, name))
521
+ break
522
+ seen_names.add(name)
523
+
524
+ return duplicates
525
+
526
+ def auto_fix(self, issue: ValidationIssue) -> bool:
527
+ """Attempt to automatically fix issue.
528
+
529
+ Supported fixes:
530
+ - Create missing config file
531
+ - Migrate legacy format
532
+ - Remove deprecated args
533
+ - Resolve relative paths to absolute
534
+
535
+ Args:
536
+ issue: Issue to fix
537
+
538
+ Returns:
539
+ True if fix succeeded, False otherwise
540
+
541
+ Example:
542
+ >>> issue = ValidationIssue(...)
543
+ >>> if issue.auto_fixable:
544
+ ... success = inspector.auto_fix(issue)
545
+ ... print(f"Fix {'succeeded' if success else 'failed'}")
546
+ """
547
+ if not issue.auto_fixable:
548
+ return False
549
+
550
+ try:
551
+ # Fix: Create missing config file
552
+ if "not found" in issue.message and issue.server_name is None:
553
+ self._create_default_config()
554
+ return True
555
+
556
+ # Fix: Migrate legacy format
557
+ if "Legacy" in issue.message:
558
+ return self._migrate_legacy_format()
559
+
560
+ # Fix: Remove deprecated args
561
+ if "deprecated arg" in issue.message and issue.server_name:
562
+ return self._remove_deprecated_args(issue.server_name)
563
+
564
+ return False
565
+
566
+ except Exception as e:
567
+ logger.error(f"Auto-fix failed: {e}")
568
+ return False
569
+
570
+ # ========================================================================
571
+ # Private Helper Methods
572
+ # ========================================================================
573
+
574
+ def _extract_servers(self, config: dict[str, Any]) -> list[MCPServerConfig]:
575
+ """Extract server configurations from config dict.
576
+
577
+ Args:
578
+ config: Configuration dictionary
579
+
580
+ Returns:
581
+ List of MCPServerConfig objects
582
+ """
583
+ servers: list[MCPServerConfig] = []
584
+
585
+ # Different platforms use different keys
586
+ server_keys = ["mcpServers", "mcp_servers", "servers"]
587
+
588
+ for key in server_keys:
589
+ if key in config and isinstance(config[key], dict):
590
+ for name, server_data in config[key].items():
591
+ if isinstance(server_data, dict):
592
+ servers.append(
593
+ MCPServerConfig(
594
+ name=name,
595
+ command=server_data.get("command", ""),
596
+ args=server_data.get("args", []),
597
+ env=server_data.get("env", {}),
598
+ description=server_data.get("description", ""),
599
+ )
600
+ )
601
+ break
602
+
603
+ return servers
604
+
605
+ def _generate_recommendations(self, servers: list[MCPServerConfig]) -> list[str]:
606
+ """Generate general recommendations for improvement.
607
+
608
+ Args:
609
+ servers: List of server configurations
610
+
611
+ Returns:
612
+ List of recommendation strings
613
+ """
614
+ recommendations: list[str] = []
615
+
616
+ # Check if any servers could use uv run
617
+ non_uv_servers = [s for s in servers if s.command != "uv"]
618
+ if non_uv_servers:
619
+ recommendations.append(
620
+ f"Consider migrating {len(non_uv_servers)} server(s) to 'uv run' "
621
+ f"for faster startup (10-30% improvement)"
622
+ )
623
+
624
+ # Check for missing descriptions
625
+ no_desc = [s for s in servers if not s.description]
626
+ if no_desc:
627
+ recommendations.append(
628
+ f"Add descriptions to {len(no_desc)} server(s) for better documentation"
629
+ )
630
+
631
+ # Check for environment variables
632
+ env_servers = [s for s in servers if s.env]
633
+ if env_servers:
634
+ recommendations.append(
635
+ f"{len(env_servers)} server(s) use environment variables - "
636
+ f"ensure secrets are properly secured"
637
+ )
638
+
639
+ return recommendations
640
+
641
+ def _suggest_command_install(self, command: str) -> str:
642
+ """Suggest how to install missing command.
643
+
644
+ Args:
645
+ command: Command that is missing
646
+
647
+ Returns:
648
+ Installation suggestion
649
+ """
650
+ suggestions = {
651
+ "uv": "Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh",
652
+ "python": "Install Python: https://python.org/downloads",
653
+ "node": "Install Node.js: https://nodejs.org",
654
+ "npm": "Install Node.js (includes npm): https://nodejs.org",
655
+ }
656
+
657
+ return suggestions.get(
658
+ command, f"Install {command} and ensure it's in your PATH"
659
+ )
660
+
661
+ def _create_default_config(self) -> None:
662
+ """Create default empty configuration file."""
663
+ self.config_path.parent.mkdir(parents=True, exist_ok=True)
664
+
665
+ if self.config_format == ConfigFormat.JSON:
666
+ default_config: dict[str, Any] = {"mcpServers": {}}
667
+ self.config_manager.write(default_config)
668
+ else:
669
+ # TOML default
670
+ default_config_toml: dict[str, Any] = {"mcp_servers": {}}
671
+ self.config_manager.write(default_config_toml)
672
+
673
+ def _migrate_legacy_format(self) -> bool:
674
+ """Migrate from line-delimited JSON to modern format.
675
+
676
+ Returns:
677
+ True if migration succeeded
678
+ """
679
+ try:
680
+ content = self.config_path.read_text(encoding="utf-8")
681
+ lines = [line.strip() for line in content.split("\n") if line.strip()]
682
+
683
+ # Parse each line as JSON object
684
+ servers: dict[str, Any] = {}
685
+ for line in lines:
686
+ server_data = json.loads(line)
687
+ name = server_data.get("name", f"server-{len(servers)}")
688
+ servers[name] = {
689
+ "command": server_data.get("command", ""),
690
+ "args": server_data.get("args", []),
691
+ "env": server_data.get("env", {}),
692
+ }
693
+
694
+ # Write modern format
695
+ modern_config = {"mcpServers": servers}
696
+ self.config_manager.write(modern_config)
697
+ return True
698
+
699
+ except Exception as e:
700
+ logger.error(f"Migration failed: {e}")
701
+ return False
702
+
703
+ def _remove_deprecated_args(self, server_name: str) -> bool:
704
+ """Remove deprecated arguments from server config.
705
+
706
+ Args:
707
+ server_name: Server to update
708
+
709
+ Returns:
710
+ True if update succeeded
711
+ """
712
+ try:
713
+ config = self.config_manager.read()
714
+ servers = self._extract_servers(config)
715
+
716
+ for server in servers:
717
+ if server.name == server_name:
718
+ # Remove deprecated args
719
+ clean_args = [
720
+ arg
721
+ for arg in server.args
722
+ if not any(dep in arg for dep in ["--legacy-mode", "--old-api"])
723
+ ]
724
+
725
+ # Update config
726
+ server_key = self._get_server_key(config)
727
+ if server_key and server.name in config[server_key]:
728
+ config[server_key][server.name]["args"] = clean_args
729
+ self.config_manager.write(config)
730
+ return True
731
+
732
+ return False
733
+
734
+ except Exception as e:
735
+ logger.error(f"Failed to remove deprecated args: {e}")
736
+ return False
737
+
738
+ def _get_server_key(self, config: dict[str, Any]) -> str | None:
739
+ """Get the key used for servers in config.
740
+
741
+ Args:
742
+ config: Configuration dictionary
743
+
744
+ Returns:
745
+ Server key name or None
746
+ """
747
+ for key in ["mcpServers", "mcp_servers", "servers"]:
748
+ if key in config:
749
+ return key
750
+ return None