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,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
|