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.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/_version_scm.py +1 -0
- mcp_ticketer/adapters/aitrackdown.py +507 -6
- mcp_ticketer/adapters/asana/adapter.py +229 -0
- mcp_ticketer/adapters/asana/mappers.py +14 -0
- mcp_ticketer/adapters/github/__init__.py +26 -0
- mcp_ticketer/adapters/github/adapter.py +3229 -0
- mcp_ticketer/adapters/github/client.py +335 -0
- mcp_ticketer/adapters/github/mappers.py +797 -0
- mcp_ticketer/adapters/github/queries.py +692 -0
- mcp_ticketer/adapters/github/types.py +460 -0
- mcp_ticketer/adapters/hybrid.py +47 -5
- mcp_ticketer/adapters/jira/__init__.py +35 -0
- mcp_ticketer/adapters/jira/adapter.py +1351 -0
- mcp_ticketer/adapters/jira/client.py +271 -0
- mcp_ticketer/adapters/jira/mappers.py +246 -0
- mcp_ticketer/adapters/jira/queries.py +216 -0
- mcp_ticketer/adapters/jira/types.py +304 -0
- mcp_ticketer/adapters/linear/adapter.py +2730 -139
- mcp_ticketer/adapters/linear/client.py +175 -3
- mcp_ticketer/adapters/linear/mappers.py +203 -8
- mcp_ticketer/adapters/linear/queries.py +280 -3
- mcp_ticketer/adapters/linear/types.py +120 -4
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -0
- mcp_ticketer/cli/adapter_diagnostics.py +3 -1
- mcp_ticketer/cli/auggie_configure.py +17 -5
- mcp_ticketer/cli/codex_configure.py +97 -61
- mcp_ticketer/cli/configure.py +1288 -105
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +13 -12
- mcp_ticketer/cli/discover.py +5 -0
- mcp_ticketer/cli/gemini_configure.py +17 -5
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/install_mcp_server.py +418 -0
- mcp_ticketer/cli/instruction_commands.py +6 -0
- mcp_ticketer/cli/main.py +267 -3175
- mcp_ticketer/cli/mcp_configure.py +821 -119
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/platform_detection.py +77 -12
- mcp_ticketer/cli/platform_installer.py +545 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/setup_command.py +795 -0
- mcp_ticketer/cli/simple_health.py +12 -10
- mcp_ticketer/cli/ticket_commands.py +705 -103
- mcp_ticketer/cli/utils.py +113 -0
- mcp_ticketer/core/__init__.py +56 -6
- mcp_ticketer/core/adapter.py +533 -2
- mcp_ticketer/core/config.py +21 -21
- mcp_ticketer/core/exceptions.py +7 -1
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +31 -19
- mcp_ticketer/core/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +480 -0
- mcp_ticketer/core/onepassword_secrets.py +1 -1
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +132 -14
- mcp_ticketer/core/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/session_state.py +176 -0
- mcp_ticketer/core/state_matcher.py +625 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/mcp/server/__main__.py +2 -1
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/main.py +106 -25
- mcp_ticketer/mcp/server/routing.py +723 -0
- mcp_ticketer/mcp/server/server_sdk.py +58 -0
- mcp_ticketer/mcp/server/tools/__init__.py +33 -11
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +5 -5
- mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
- mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
- mcp_ticketer/mcp/server/tools/config_tools.py +1391 -145
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
- mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +209 -97
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1107 -124
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
- mcp_ticketer/queue/queue.py +68 -0
- mcp_ticketer/queue/worker.py +1 -1
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.2.13.dist-info/METADATA +1396 -0
- mcp_ticketer-2.2.13.dist-info/RECORD +158 -0
- mcp_ticketer-2.2.13.dist-info/top_level.txt +2 -0
- py_mcp_installer/examples/phase3_demo.py +178 -0
- py_mcp_installer/scripts/manage_version.py +54 -0
- py_mcp_installer/setup.py +6 -0
- py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
- py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
- py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
- py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
- py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
- py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
- py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
- py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
- py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
- py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
- py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
- py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
- py_mcp_installer/src/py_mcp_installer/types.py +222 -0
- py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
- py_mcp_installer/tests/__init__.py +0 -0
- py_mcp_installer/tests/platforms/__init__.py +0 -0
- py_mcp_installer/tests/test_platform_detector.py +17 -0
- mcp_ticketer/adapters/github.py +0 -1574
- mcp_ticketer/adapters/jira.py +0 -1258
- mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
- mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
- mcp_ticketer-0.12.0.dist-info/top_level.txt +0 -1
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
- {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
|