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
@@ -26,12 +26,22 @@ Performance: Configuration is cached in memory by ConfigResolver,
26
26
  so repeated reads are fast (O(1) after first load).
27
27
  """
28
28
 
29
+ import logging
30
+ import warnings
29
31
  from pathlib import Path
30
32
  from typing import Any
31
33
 
32
- from ....core.project_config import AdapterType, ConfigResolver, TicketerConfig
34
+ from ....core.project_config import (
35
+ AdapterType,
36
+ ConfigResolver,
37
+ ConfigValidator,
38
+ TicketerConfig,
39
+ )
40
+ from ....core.registry import AdapterRegistry
33
41
  from ..server_sdk import mcp
34
42
 
43
+ logger = logging.getLogger(__name__)
44
+
35
45
 
36
46
  def get_resolver() -> ConfigResolver:
37
47
  """Get or create the configuration resolver.
@@ -50,43 +60,370 @@ def get_resolver() -> ConfigResolver:
50
60
  return ConfigResolver(project_path=Path.cwd())
51
61
 
52
62
 
63
+ def _safe_load_config() -> TicketerConfig:
64
+ """Safely load project configuration, preserving existing adapters.
65
+
66
+ This function prevents data loss when updating config fields by:
67
+ 1. Attempting to load existing configuration
68
+ 2. If file doesn't exist: create new empty config (first-time setup OK)
69
+ 3. If file exists but fails to load: raise error to prevent data wipe
70
+
71
+ Returns:
72
+ Loaded or new TicketerConfig instance
73
+
74
+ Raises:
75
+ RuntimeError: If config file exists but cannot be loaded
76
+
77
+ Design Rationale:
78
+ The pattern `config = resolver.load_project_config() or TicketerConfig()`
79
+ is DANGEROUS because load_project_config() returns None on ANY failure
80
+ (file read error, JSON parse error, etc), which creates an empty config
81
+ and wipes all adapter configurations when saved.
82
+
83
+ This function prevents data loss by explicitly checking if the file
84
+ exists before deciding whether to create a new config.
85
+ """
86
+ resolver = get_resolver()
87
+ config_path = resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH
88
+
89
+ # Try to load existing config
90
+ config = resolver.load_project_config()
91
+
92
+ # If config loaded successfully, return it
93
+ if config is not None:
94
+ return config
95
+
96
+ # Config is None - need to determine if this is first-time setup or an error
97
+ if config_path.exists():
98
+ # File exists but failed to load - this is an error condition
99
+ # DO NOT create empty config and wipe existing data
100
+ raise RuntimeError(
101
+ f"Configuration file exists at {config_path} but failed to load. "
102
+ f"This may indicate a corrupted or invalid JSON file. "
103
+ f"Please check the file manually before retrying. "
104
+ f"To prevent data loss, this operation was aborted."
105
+ )
106
+
107
+ # File doesn't exist - first-time setup, safe to create new config
108
+ logger.info(f"No configuration file found at {config_path}, creating new config")
109
+ return TicketerConfig()
110
+
111
+
53
112
  @mcp.tool()
54
- async def config_set_primary_adapter(adapter: str) -> dict[str, Any]:
55
- """Set the default adapter for ticket operations.
113
+ async def config(
114
+ action: str,
115
+ key: str | None = None,
116
+ value: Any | None = None,
117
+ adapter_name: str | None = None,
118
+ adapter: str | None = None,
119
+ adapter_type: str | None = None,
120
+ credentials: dict[str, Any] | None = None,
121
+ set_as_default: bool = True,
122
+ test_connection: bool = True,
123
+ # Explicitly define optional parameters (previously in **kwargs)
124
+ project_key: str | None = None,
125
+ user_email: str | None = None,
126
+ ) -> dict[str, Any]:
127
+ """Unified configuration management tool with action-based routing (v2.0.0).
56
128
 
57
- Updates the project-local configuration (.mcp-ticketer/config.json)
58
- to use the specified adapter as the default for all ticket operations.
129
+ Single tool for all 16 configuration operations. Consolidates all config_*
130
+ tools into one interface for ~7,200 token savings (90% reduction).
59
131
 
60
132
  Args:
61
- adapter: Adapter name to set as primary. Must be one of:
62
- - "aitrackdown" (file-based tracking)
63
- - "linear" (Linear.app)
64
- - "github" (GitHub Issues)
65
- - "jira" (Atlassian JIRA)
133
+ action: Operation to perform. Valid values:
134
+ - "get": Get current configuration
135
+ - "set": Set a configuration value (requires key and value)
136
+ - "set_project_from_url": Set default project from URL with validation (requires value=URL)
137
+ - "validate": Validate all adapter configurations
138
+ - "test": Test adapter connectivity (requires adapter_name)
139
+ - "list_adapters": List all available adapters
140
+ - "get_requirements": Get adapter requirements (requires adapter)
141
+ - "setup_wizard": Interactive adapter setup (requires adapter_type and credentials)
142
+ key: Configuration key (for action="set"). Valid values:
143
+ - "adapter", "project", "user", "tags", "team", "cycle", "epic", "assignment_labels"
144
+ value: Value to set (for action="set", type depends on key)
145
+ adapter_name: Adapter to test (for action="test")
146
+ adapter: Adapter to get requirements for (for action="get_requirements")
147
+ adapter_type: Adapter type for setup (for action="setup_wizard")
148
+ credentials: Adapter credentials dict (for action="setup_wizard")
149
+ set_as_default: Set adapter as default (for action="setup_wizard", default: True)
150
+ test_connection: Test connection during setup (for action="setup_wizard", default: True)
151
+ project_key: Project key for JIRA adapter (for action="set" with key="project")
152
+ user_email: User email for adapter-specific user identification (for action="set" with key="user")
66
153
 
67
154
  Returns:
68
- Dictionary containing:
69
- - status: "completed" or "error"
70
- - message: Success or error message
71
- - previous_adapter: Previous default adapter (if successful)
72
- - new_adapter: New default adapter (if successful)
73
- - error: Error details (if failed)
74
-
75
- Example:
76
- >>> result = await config_set_primary_adapter("linear")
77
- >>> print(result)
78
- {
79
- "status": "completed",
80
- "message": "Default adapter set to 'linear'",
81
- "previous_adapter": "aitrackdown",
82
- "new_adapter": "linear"
155
+ Response dict with status and action-specific data
156
+
157
+ Examples:
158
+ # Get configuration
159
+ config(action="get")
160
+
161
+ # Set default adapter
162
+ config(action="set", key="adapter", value="linear")
163
+
164
+ # Validate all adapters
165
+ config(action="validate")
166
+
167
+ # Test adapter connection
168
+ config(action="test", adapter_name="linear")
169
+
170
+ # List all adapters
171
+ config(action="list_adapters")
172
+
173
+ # Get adapter requirements
174
+ config(action="get_requirements", adapter="linear")
175
+
176
+ # Setup wizard (interactive configuration)
177
+ config(action="setup_wizard", adapter_type="linear",
178
+ credentials={"api_key": "...", "team_key": "ENG"})
179
+
180
+ Migration from deprecated tools:
181
+ - config_get() → config(action="get")
182
+ - config_set(key="adapter", value="linear") → config(action="set", key="adapter", value="linear")
183
+ - config_set_primary_adapter("linear") → config(action="set", key="adapter", value="linear")
184
+ - config_set_default_project("PROJ") → config(action="set", key="project", value="PROJ")
185
+ - config_set_default_user("user@ex.com") → config(action="set", key="user", value="user@ex.com")
186
+ - config_set_default_tags(["bug"]) → config(action="set", key="tags", value=["bug"])
187
+ - config_set_default_team("ENG") → config(action="set", key="team", value="ENG")
188
+ - config_set_default_cycle("S23") → config(action="set", key="cycle", value="S23")
189
+ - config_set_default_epic("EP-1") → config(action="set", key="epic", value="EP-1")
190
+ - config_set_assignment_labels(["my"]) → config(action="set", key="assignment_labels", value=["my"])
191
+ - config_validate() → config(action="validate")
192
+ - config_test_adapter("linear") → config(action="test", adapter_name="linear")
193
+ - config_list_adapters() → config(action="list_adapters")
194
+ - config_get_adapter_requirements("linear") → config(action="get_requirements", adapter="linear")
195
+ - config_setup_wizard(...) → config(action="setup_wizard", ...)
196
+
197
+ Token Savings:
198
+ Before: 16 tools × ~500 tokens = ~8,000 tokens
199
+ After: 1 unified tool × ~800 tokens = ~800 tokens
200
+ Savings: ~7,200 tokens (90% reduction)
201
+
202
+ See: docs/mcp-api-reference.md#config-response-format
203
+ """
204
+ action_lower = action.lower()
205
+
206
+ # Route based on action
207
+ if action_lower == "get":
208
+ return await config_get()
209
+ elif action_lower == "set_project_from_url":
210
+ if value is None:
211
+ return {
212
+ "status": "error",
213
+ "error": "Parameter 'value' (project URL) is required for action='set_project_from_url'",
214
+ "hint": "Use config(action='set_project_from_url', value='https://linear.app/...')",
215
+ }
216
+ return await config_set_project_from_url(
217
+ project_url=str(value), test_connection=test_connection
218
+ )
219
+ elif action_lower == "set":
220
+ if key is None:
221
+ return {
222
+ "status": "error",
223
+ "error": "Parameter 'key' is required for action='set'",
224
+ "hint": "Use config(action='set', key='adapter', value='linear')",
225
+ }
226
+ if value is None:
227
+ return {
228
+ "status": "error",
229
+ "error": "Parameter 'value' is required for action='set'",
230
+ "hint": "Use config(action='set', key='adapter', value='linear')",
231
+ }
232
+
233
+ # Build extra params dict from non-None values
234
+ extra_params = {}
235
+ if project_key is not None:
236
+ extra_params["project_key"] = project_key
237
+ if user_email is not None:
238
+ extra_params["user_email"] = user_email
239
+
240
+ return await config_set(key=key, value=value, **extra_params)
241
+ elif action_lower == "validate":
242
+ return await config_validate()
243
+ elif action_lower == "test":
244
+ if adapter_name is None:
245
+ return {
246
+ "status": "error",
247
+ "error": "Parameter 'adapter_name' is required for action='test'",
248
+ "hint": "Use config(action='test', adapter_name='linear')",
249
+ }
250
+ return await config_test_adapter(adapter_name=adapter_name)
251
+ elif action_lower == "list_adapters":
252
+ return await config_list_adapters()
253
+ elif action_lower == "get_requirements":
254
+ if adapter is None:
255
+ return {
256
+ "status": "error",
257
+ "error": "Parameter 'adapter' is required for action='get_requirements'",
258
+ "hint": "Use config(action='get_requirements', adapter='linear')",
259
+ }
260
+ return await config_get_adapter_requirements(adapter=adapter)
261
+ elif action_lower == "setup_wizard":
262
+ if adapter_type is None:
263
+ return {
264
+ "status": "error",
265
+ "error": "Parameter 'adapter_type' is required for action='setup_wizard'",
266
+ "hint": "Use config(action='setup_wizard', adapter_type='linear', credentials={...})",
267
+ }
268
+ if credentials is None:
269
+ return {
270
+ "status": "error",
271
+ "error": "Parameter 'credentials' is required for action='setup_wizard'",
272
+ "hint": "Use config(action='setup_wizard', adapter_type='linear', credentials={...})",
273
+ }
274
+ return await config_setup_wizard(
275
+ adapter_type=adapter_type,
276
+ credentials=credentials,
277
+ set_as_default=set_as_default,
278
+ test_connection=test_connection,
279
+ )
280
+ else:
281
+ valid_actions = [
282
+ "get",
283
+ "set",
284
+ "set_project_from_url",
285
+ "validate",
286
+ "test",
287
+ "list_adapters",
288
+ "get_requirements",
289
+ "setup_wizard",
290
+ ]
291
+ return {
292
+ "status": "error",
293
+ "error": f"Invalid action '{action}'. Must be one of: {', '.join(valid_actions)}",
294
+ "valid_actions": valid_actions,
295
+ "hint": "Use config(action='get') to see current configuration",
83
296
  }
84
297
 
85
- Error Conditions:
86
- - Invalid adapter name: Returns error with valid options
87
- - Configuration file write failure: Returns error with file path
88
298
 
299
+ async def config_set(
300
+ key: str,
301
+ value: Any,
302
+ **kwargs: Any,
303
+ ) -> dict[str, Any]:
304
+ """Set configuration value (unified setter for all config options).
305
+
306
+ .. deprecated::
307
+ Use config(action="set", key="...", value=...) instead.
308
+ This tool will be removed in a future version.
309
+
310
+ This tool consolidates all config_set_* operations into a single interface.
311
+ Use the 'key' parameter to specify which configuration to set.
312
+
313
+ Args:
314
+ key: Configuration key to set. Valid values:
315
+ - "adapter": Set default adapter (value: adapter name string)
316
+ - "project": Set default project/epic (value: project ID string)
317
+ - "user": Set default user/assignee (value: user ID string)
318
+ - "tags": Set default tags (value: list of tag strings)
319
+ - "team": Set default team (value: team ID string)
320
+ - "cycle": Set default cycle/sprint (value: cycle ID string)
321
+ - "epic": Set default epic (alias for "project", value: epic ID string)
322
+ - "assignment_labels": Set assignment labels (value: list of label strings)
323
+ value: Value to set (type depends on key)
324
+ **kwargs: Additional key-specific parameters (e.g., project_key, user_email)
325
+
326
+ Returns:
327
+ ConfigResponse with status, message, previous/new values, config_path
328
+
329
+ Examples:
330
+ # Set default adapter
331
+ config_set(key="adapter", value="linear")
332
+
333
+ # Set default project
334
+ config_set(key="project", value="PROJ-123")
335
+
336
+ # Set default tags
337
+ config_set(key="tags", value=["bug", "high-priority"])
338
+
339
+ # Set default user
340
+ config_set(key="user", value="user@example.com")
341
+
342
+ Migration from old tools:
343
+ - config_set_primary_adapter(adapter="linear") → config_set(key="adapter", value="linear")
344
+ - config_set_default_project(project_id="PROJ") → config_set(key="project", value="PROJ")
345
+ - config_set_default_user(user_id="user@ex.com") → config_set(key="user", value="user@ex.com")
346
+ - config_set_default_tags(tags=["bug"]) → config_set(key="tags", value=["bug"])
347
+ - config_set_default_team(team_id="ENG") → config_set(key="team", value="ENG")
348
+ - config_set_default_cycle(cycle_id="S23") → config_set(key="cycle", value="S23")
349
+ - config_set_default_epic(epic_id="EP-1") → config_set(key="epic", value="EP-1")
350
+ - config_set_assignment_labels(labels=["my"]) → config_set(key="assignment_labels", value=["my"])
351
+
352
+ See: docs/mcp-api-reference.md#config-response-format
353
+ """
354
+ warnings.warn(
355
+ "config_set is deprecated. Use config(action='set', key=key, value=value) instead.",
356
+ DeprecationWarning,
357
+ stacklevel=2,
358
+ )
359
+
360
+ key_lower = key.lower()
361
+
362
+ # Route to appropriate handler based on key
363
+ if key_lower == "adapter":
364
+ return await config_set_primary_adapter(adapter=str(value))
365
+ elif key_lower in ("project", "epic"):
366
+ project_key = kwargs.get("project_key")
367
+ return await config_set_default_project(
368
+ project_id=str(value), project_key=project_key
369
+ )
370
+ elif key_lower == "user":
371
+ user_email = kwargs.get("user_email")
372
+ return await config_set_default_user(user_id=str(value), user_email=user_email)
373
+ elif key_lower == "tags":
374
+ if not isinstance(value, list):
375
+ return {
376
+ "status": "error",
377
+ "error": f"Value for key 'tags' must be a list, got {type(value).__name__}",
378
+ }
379
+ return await config_set_default_tags(tags=value)
380
+ elif key_lower == "team":
381
+ return await config_set_default_team(team_id=str(value))
382
+ elif key_lower == "cycle":
383
+ return await config_set_default_cycle(cycle_id=str(value))
384
+ elif key_lower == "assignment_labels":
385
+ if not isinstance(value, list):
386
+ return {
387
+ "status": "error",
388
+ "error": f"Value for key 'assignment_labels' must be a list, got {type(value).__name__}",
389
+ }
390
+ return await config_set_assignment_labels(labels=value)
391
+ else:
392
+ valid_keys = [
393
+ "adapter",
394
+ "project",
395
+ "epic",
396
+ "user",
397
+ "tags",
398
+ "team",
399
+ "cycle",
400
+ "assignment_labels",
401
+ ]
402
+ return {
403
+ "status": "error",
404
+ "error": f"Invalid configuration key '{key}'. Must be one of: {', '.join(valid_keys)}",
405
+ "valid_keys": valid_keys,
406
+ "hint": "Use config_get() to see current configuration",
407
+ }
408
+
409
+
410
+ async def config_set_primary_adapter(adapter: str) -> dict[str, Any]:
411
+ """Set the default adapter for ticket operations.
412
+
413
+ .. deprecated::
414
+ Use config_set(key="adapter", value="adapter_name") instead.
415
+ This tool will be removed in a future version.
416
+
417
+ Args: adapter - Adapter type (aitrackdown, linear, github, jira)
418
+ Returns: ConfigResponse with previous/new adapter, config_path
419
+ See: docs/mcp-api-reference.md#config-response-format
420
+ docs/mcp-api-reference.md#adapter-types
89
421
  """
422
+ warnings.warn(
423
+ "config_set_primary_adapter is deprecated. Use config_set(key='adapter', value=adapter) instead.",
424
+ DeprecationWarning,
425
+ stacklevel=2,
426
+ )
90
427
  try:
91
428
  # Validate adapter name against registry
92
429
  valid_adapters = [adapter_type.value for adapter_type in AdapterType]
@@ -97,9 +434,8 @@ async def config_set_primary_adapter(adapter: str) -> dict[str, Any]:
97
434
  "valid_adapters": valid_adapters,
98
435
  }
99
436
 
100
- # Load current configuration
101
- resolver = get_resolver()
102
- config = resolver.load_project_config() or TicketerConfig()
437
+ # Load current configuration safely (preserves adapters)
438
+ config = _safe_load_config()
103
439
 
104
440
  # Store previous adapter for response
105
441
  previous_adapter = config.default_adapter
@@ -108,6 +444,7 @@ async def config_set_primary_adapter(adapter: str) -> dict[str, Any]:
108
444
  config.default_adapter = adapter.lower()
109
445
 
110
446
  # Save configuration
447
+ resolver = get_resolver()
111
448
  resolver.save_project_config(config)
112
449
 
113
450
  return {
@@ -124,49 +461,29 @@ async def config_set_primary_adapter(adapter: str) -> dict[str, Any]:
124
461
  }
125
462
 
126
463
 
127
- @mcp.tool()
128
464
  async def config_set_default_project(
129
465
  project_id: str,
130
466
  project_key: str | None = None,
131
467
  ) -> dict[str, Any]:
132
468
  """Set the default project/epic for new tickets.
133
469
 
134
- Updates the project-local configuration to automatically assign new tickets
135
- to the specified project or epic. This is useful for teams working primarily
136
- on a single project or feature area.
137
-
138
- Args:
139
- project_id: Project or epic ID to set as default (required)
140
- project_key: Optional project key (for adapters that use keys vs IDs)
141
-
142
- Returns:
143
- Dictionary containing:
144
- - status: "completed" or "error"
145
- - message: Success or error message
146
- - previous_project: Previous default project (if any)
147
- - new_project: New default project ID
148
- - error: Error details (if failed)
149
-
150
- Example:
151
- >>> result = await config_set_default_project("PROJ-123")
152
- >>> print(result)
153
- {
154
- "status": "completed",
155
- "message": "Default project set to 'PROJ-123'",
156
- "previous_project": None,
157
- "new_project": "PROJ-123"
158
- }
159
-
160
- Usage Notes:
161
- - This sets both default_project and default_epic (for backward compatibility)
162
- - Empty string or null clears the default project
163
- - Project ID is not validated (allows flexibility across adapters)
470
+ .. deprecated::
471
+ Use config_set(key="project", value="project_id") instead.
472
+ This tool will be removed in a future version.
164
473
 
474
+ Args: project_id (required), project_key (optional for key-based adapters)
475
+ Returns: ConfigResponse with previous/new project
476
+ Note: Sets both default_project and default_epic for backward compatibility
477
+ See: docs/mcp-api-reference.md#config-response-format
165
478
  """
479
+ warnings.warn(
480
+ "config_set_default_project is deprecated. Use config_set(key='project', value=project_id) instead.",
481
+ DeprecationWarning,
482
+ stacklevel=2,
483
+ )
166
484
  try:
167
- # Load current configuration
168
- resolver = get_resolver()
169
- config = resolver.load_project_config() or TicketerConfig()
485
+ # Load current configuration safely (preserves adapters)
486
+ config = _safe_load_config()
170
487
 
171
488
  # Store previous project for response
172
489
  previous_project = config.default_project or config.default_epic
@@ -176,6 +493,7 @@ async def config_set_default_project(
176
493
  config.default_epic = project_id if project_id else None
177
494
 
178
495
  # Save configuration
496
+ resolver = get_resolver()
179
497
  resolver.save_project_config(config)
180
498
 
181
499
  return {
@@ -196,59 +514,28 @@ async def config_set_default_project(
196
514
  }
197
515
 
198
516
 
199
- @mcp.tool()
200
517
  async def config_set_default_user(
201
518
  user_id: str,
202
519
  user_email: str | None = None,
203
520
  ) -> dict[str, Any]:
204
521
  """Set the default assignee for new tickets.
205
522
 
206
- Updates the project-local configuration to automatically assign new tickets
207
- to the specified user. Supports both user IDs and email addresses depending
208
- on adapter requirements.
209
-
210
- Args:
211
- user_id: User identifier or email to set as default assignee (required)
212
- user_email: Optional email (for adapters that require separate email field)
213
-
214
- Returns:
215
- Dictionary containing:
216
- - status: "completed" or "error"
217
- - message: Success or error message
218
- - previous_user: Previous default user (if any)
219
- - new_user: New default user ID
220
- - error: Error details (if failed)
221
-
222
- Example:
223
- >>> result = await config_set_default_user("user123")
224
- >>> print(result)
225
- {
226
- "status": "completed",
227
- "message": "Default user set to 'user123'",
228
- "previous_user": None,
229
- "new_user": "user123"
230
- }
231
-
232
- Example with email:
233
- >>> result = await config_set_default_user("user@example.com")
234
- >>> print(result)
235
- {
236
- "status": "completed",
237
- "message": "Default user set to 'user@example.com'",
238
- "previous_user": "old_user@example.com",
239
- "new_user": "user@example.com"
240
- }
241
-
242
- Usage Notes:
243
- - User ID/email is not validated (allows flexibility across adapters)
244
- - Empty string or null clears the default user
245
- - Some adapters prefer email, others prefer user UUID
523
+ .. deprecated::
524
+ Use config_set(key="user", value="user_id") instead.
525
+ This tool will be removed in a future version.
246
526
 
527
+ Args: user_id (ID/email/username), user_email (optional for adapters needing both)
528
+ Returns: ConfigResponse with previous/new user
529
+ See: docs/mcp-api-reference.md#user-identifiers
247
530
  """
531
+ warnings.warn(
532
+ "config_set_default_user is deprecated. Use config_set(key='user', value=user_id) instead.",
533
+ DeprecationWarning,
534
+ stacklevel=2,
535
+ )
248
536
  try:
249
- # Load current configuration
250
- resolver = get_resolver()
251
- config = resolver.load_project_config() or TicketerConfig()
537
+ # Load current configuration safely (preserves adapters)
538
+ config = _safe_load_config()
252
539
 
253
540
  # Store previous user for response
254
541
  previous_user = config.default_user
@@ -257,6 +544,7 @@ async def config_set_default_user(
257
544
  config.default_user = user_id if user_id else None
258
545
 
259
546
  # Save configuration
547
+ resolver = get_resolver()
260
548
  resolver.save_project_config(config)
261
549
 
262
550
  return {
@@ -277,47 +565,22 @@ async def config_set_default_user(
277
565
  }
278
566
 
279
567
 
280
- @mcp.tool()
281
568
  async def config_get() -> dict[str, Any]:
282
569
  """Get current configuration settings.
283
570
 
284
- Retrieves the current project-local configuration including default adapter,
285
- project, user, and all adapter-specific settings.
286
-
287
- Returns:
288
- Dictionary containing:
289
- - status: "completed" or "error"
290
- - config: Complete configuration dictionary including:
291
- - default_adapter: Primary adapter name
292
- - default_project: Default project/epic ID (if set)
293
- - default_user: Default assignee (if set)
294
- - adapters: All adapter configurations
295
- - hybrid_mode: Hybrid mode settings (if enabled)
296
- - config_path: Path to configuration file
297
- - error: Error details (if failed)
298
-
299
- Example:
300
- >>> result = await config_get()
301
- >>> print(result)
302
- {
303
- "status": "completed",
304
- "config": {
305
- "default_adapter": "linear",
306
- "default_project": "PROJ-123",
307
- "default_user": "user@example.com",
308
- "adapters": {
309
- "linear": {"api_key": "***", "team_id": "..."}
310
- }
311
- },
312
- "config_path": "/project/.mcp-ticketer/config.json"
313
- }
314
-
315
- Usage Notes:
316
- - Sensitive values (API keys) are masked in the response
317
- - Returns default values if no configuration file exists
318
- - Configuration is merged from multiple sources (env vars, .env files, config.json)
571
+ .. deprecated::
572
+ Use config(action="get") instead.
573
+ This tool will be removed in a future version.
319
574
 
575
+ Returns: Complete config dict with default_adapter, default_project, default_user, adapters
576
+ Note: Sensitive values (API keys) masked; merges env vars, .env, config.json
577
+ See: docs/mcp-api-reference.md#config-response-format
320
578
  """
579
+ warnings.warn(
580
+ "config_get is deprecated. Use config(action='get') instead.",
581
+ DeprecationWarning,
582
+ stacklevel=2,
583
+ )
321
584
  try:
322
585
  # Load current configuration
323
586
  resolver = get_resolver()
@@ -350,6 +613,989 @@ async def config_get() -> dict[str, Any]:
350
613
  }
351
614
 
352
615
 
616
+ async def config_set_default_tags(
617
+ tags: list[str],
618
+ ) -> dict[str, Any]:
619
+ """Set default tags for new ticket creation.
620
+
621
+ .. deprecated::
622
+ Use config_set(key="tags", value=["tag1", "tag2"]) instead.
623
+ This tool will be removed in a future version.
624
+
625
+ Args: tags - List of tag names (2-50 chars each, merged with user tags at creation)
626
+ Returns: ConfigResponse with default_tags list
627
+ See: docs/mcp-api-reference.md#config-response-format
628
+ """
629
+ warnings.warn(
630
+ "config_set_default_tags is deprecated. Use config_set(key='tags', value=tags) instead.",
631
+ DeprecationWarning,
632
+ stacklevel=2,
633
+ )
634
+ try:
635
+ # Validate tags
636
+ if not tags:
637
+ return {
638
+ "status": "error",
639
+ "error": "Please provide at least one tag",
640
+ }
641
+
642
+ for tag in tags:
643
+ if not tag or len(tag.strip()) < 2:
644
+ return {
645
+ "status": "error",
646
+ "error": f"Tag '{tag}' must be at least 2 characters",
647
+ }
648
+ if len(tag.strip()) > 50:
649
+ return {
650
+ "status": "error",
651
+ "error": f"Tag '{tag}' is too long (max 50 characters)",
652
+ }
653
+
654
+ # Load current configuration safely (preserves adapters)
655
+ config = _safe_load_config()
656
+
657
+ # Update config
658
+ config.default_tags = [tag.strip() for tag in tags]
659
+
660
+ # Save configuration
661
+ resolver = get_resolver()
662
+ resolver.save_project_config(config)
663
+
664
+ return {
665
+ "status": "completed",
666
+ "default_tags": config.default_tags,
667
+ "message": f"Default tags set to: {', '.join(config.default_tags)}",
668
+ "config_path": str(resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH),
669
+ }
670
+ except Exception as e:
671
+ return {
672
+ "status": "error",
673
+ "error": f"Failed to set default tags: {str(e)}",
674
+ }
675
+
676
+
677
+ async def config_set_default_team(
678
+ team_id: str,
679
+ ) -> dict[str, Any]:
680
+ """Set the default team for ticket operations.
681
+
682
+ .. deprecated::
683
+ Use config_set(key="team", value="team_id") instead.
684
+ This tool will be removed in a future version.
685
+
686
+ Args: team_id - Team ID/key (e.g., "ENG", UUID for Linear multi-team workspaces)
687
+ Returns: ConfigResponse with previous/new team
688
+ Note: Helps scope ticket_list and ticket_search operations
689
+ See: docs/mcp-api-reference.md#config-response-format
690
+ """
691
+ warnings.warn(
692
+ "config_set_default_team is deprecated. Use config_set(key='team', value=team_id) instead.",
693
+ DeprecationWarning,
694
+ stacklevel=2,
695
+ )
696
+ try:
697
+ # Validate team ID
698
+ if not team_id or len(team_id.strip()) < 1:
699
+ return {
700
+ "status": "error",
701
+ "error": "Team ID must be at least 1 character",
702
+ }
703
+
704
+ # Load current configuration safely (preserves adapters)
705
+ config = _safe_load_config()
706
+
707
+ # Store previous team for response
708
+ previous_team = config.default_team
709
+
710
+ # Update default team
711
+ config.default_team = team_id.strip()
712
+
713
+ # Save configuration
714
+ resolver = get_resolver()
715
+ resolver.save_project_config(config)
716
+
717
+ return {
718
+ "status": "completed",
719
+ "message": f"Default team set to '{team_id}'",
720
+ "previous_team": previous_team,
721
+ "new_team": config.default_team,
722
+ "config_path": str(resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH),
723
+ }
724
+ except Exception as e:
725
+ return {
726
+ "status": "error",
727
+ "error": f"Failed to set default team: {str(e)}",
728
+ }
729
+
730
+
731
+ async def config_set_default_cycle(
732
+ cycle_id: str,
733
+ ) -> dict[str, Any]:
734
+ """Set the default cycle/sprint for ticket operations.
735
+
736
+ .. deprecated::
737
+ Use config_set(key="cycle", value="cycle_id") instead.
738
+ This tool will be removed in a future version.
739
+
740
+ Args: cycle_id - Sprint/cycle ID (e.g., "Sprint 23", UUID for sprint planning)
741
+ Returns: ConfigResponse with previous/new cycle
742
+ Note: Helps scope ticket_list and ticket_search to active sprint
743
+ See: docs/mcp-api-reference.md#config-response-format
744
+ """
745
+ warnings.warn(
746
+ "config_set_default_cycle is deprecated. Use config_set(key='cycle', value=cycle_id) instead.",
747
+ DeprecationWarning,
748
+ stacklevel=2,
749
+ )
750
+ try:
751
+ # Validate cycle ID
752
+ if not cycle_id or len(cycle_id.strip()) < 1:
753
+ return {
754
+ "status": "error",
755
+ "error": "Cycle ID must be at least 1 character",
756
+ }
757
+
758
+ # Load current configuration safely (preserves adapters)
759
+ config = _safe_load_config()
760
+
761
+ # Store previous cycle for response
762
+ previous_cycle = config.default_cycle
763
+
764
+ # Update default cycle
765
+ config.default_cycle = cycle_id.strip()
766
+
767
+ # Save configuration
768
+ resolver = get_resolver()
769
+ resolver.save_project_config(config)
770
+
771
+ return {
772
+ "status": "completed",
773
+ "message": f"Default cycle set to '{cycle_id}'",
774
+ "previous_cycle": previous_cycle,
775
+ "new_cycle": config.default_cycle,
776
+ "config_path": str(resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH),
777
+ }
778
+ except Exception as e:
779
+ return {
780
+ "status": "error",
781
+ "error": f"Failed to set default cycle: {str(e)}",
782
+ }
783
+
784
+
785
+ async def config_set_default_epic(
786
+ epic_id: str,
787
+ ) -> dict[str, Any]:
788
+ """Set default epic/project for new ticket creation.
789
+
790
+ .. deprecated::
791
+ Use config_set(key="epic", value="epic_id") instead.
792
+ This tool will be removed in a future version.
793
+
794
+ Args: epic_id - Epic/project ID (alias for config_set_default_project)
795
+ Returns: ConfigResponse with default_epic and default_project (both set for compatibility)
796
+ See: docs/mcp-api-reference.md#config-response-format
797
+ """
798
+ warnings.warn(
799
+ "config_set_default_epic is deprecated. Use config_set(key='epic', value=epic_id) instead.",
800
+ DeprecationWarning,
801
+ stacklevel=2,
802
+ )
803
+ try:
804
+ # Validate epic ID
805
+ if not epic_id or len(epic_id.strip()) < 2:
806
+ return {
807
+ "status": "error",
808
+ "error": "Epic/project ID must be at least 2 characters",
809
+ }
810
+
811
+ # Load current configuration safely (preserves adapters)
812
+ config = _safe_load_config()
813
+
814
+ # Update config (set both for compatibility)
815
+ config.default_epic = epic_id.strip()
816
+ config.default_project = epic_id.strip()
817
+
818
+ # Save configuration
819
+ resolver = get_resolver()
820
+ resolver.save_project_config(config)
821
+
822
+ return {
823
+ "status": "completed",
824
+ "default_epic": config.default_epic,
825
+ "default_project": config.default_project,
826
+ "message": f"Default epic/project set to: {epic_id}",
827
+ "config_path": str(resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH),
828
+ }
829
+ except Exception as e:
830
+ return {
831
+ "status": "error",
832
+ "error": f"Failed to set default epic: {str(e)}",
833
+ }
834
+
835
+
836
+ async def config_set_assignment_labels(labels: list[str]) -> dict[str, Any]:
837
+ """Set labels that indicate ticket assignment to user.
838
+
839
+ .. deprecated::
840
+ Use config_set(key="assignment_labels", value=["label1", "label2"]) instead.
841
+ This tool will be removed in a future version.
842
+
843
+ Args: labels - Label names indicating user ownership (e.g., ["my-work", "in-progress"])
844
+ Returns: ConfigResponse with assignment_labels list
845
+ Note: Used by check_open_tickets to find work beyond formal assignment field
846
+ See: docs/mcp-api-reference.md#config-response-format
847
+ """
848
+ warnings.warn(
849
+ "config_set_assignment_labels is deprecated. Use config_set(key='assignment_labels', value=labels) instead.",
850
+ DeprecationWarning,
851
+ stacklevel=2,
852
+ )
853
+ try:
854
+ # Validate label format
855
+ for label in labels:
856
+ if not label or len(label) < 2 or len(label) > 50:
857
+ return {
858
+ "status": "error",
859
+ "error": f"Invalid label '{label}': must be 2-50 characters",
860
+ }
861
+
862
+ # Load current configuration safely (preserves adapters)
863
+ config = _safe_load_config()
864
+
865
+ config.assignment_labels = labels if labels else None
866
+
867
+ # Save configuration
868
+ resolver = get_resolver()
869
+ resolver.save_project_config(config)
870
+
871
+ config_path = Path.cwd() / ".mcp-ticketer" / "config.json"
872
+
873
+ return {
874
+ "status": "completed",
875
+ "message": (
876
+ f"Assignment labels set to: {', '.join(labels)}"
877
+ if labels
878
+ else "Assignment labels cleared"
879
+ ),
880
+ "assignment_labels": labels,
881
+ "config_path": str(config_path),
882
+ }
883
+ except Exception as e:
884
+ return {
885
+ "status": "error",
886
+ "error": f"Failed to set assignment labels: {str(e)}",
887
+ }
888
+
889
+
890
+ async def config_validate() -> dict[str, Any]:
891
+ """Validate all adapter configurations (structure only, no connectivity test).
892
+
893
+ .. deprecated::
894
+ Use config(action="validate") instead.
895
+ This tool will be removed in a future version.
896
+
897
+ Returns: ValidationResponse with validation_results, all_valid, issues list
898
+ Note: Checks required fields, formats (API keys, URLs, emails). Use config_test_adapter() for connectivity.
899
+ See: docs/mcp-api-reference.md#validation-response-format
900
+ """
901
+ warnings.warn(
902
+ "config_validate is deprecated. Use config(action='validate') instead.",
903
+ DeprecationWarning,
904
+ stacklevel=2,
905
+ )
906
+ try:
907
+ resolver = get_resolver()
908
+ config = resolver.load_project_config() or TicketerConfig()
909
+
910
+ if not config.adapters:
911
+ return {
912
+ "status": "completed",
913
+ "validation_results": {},
914
+ "all_valid": True,
915
+ "issues": [],
916
+ "message": "No adapters configured",
917
+ }
918
+
919
+ results = {}
920
+ issues = []
921
+
922
+ for adapter_name, adapter_config in config.adapters.items():
923
+ is_valid, error = ConfigValidator.validate(
924
+ adapter_name, adapter_config.to_dict()
925
+ )
926
+
927
+ results[adapter_name] = {
928
+ "valid": is_valid,
929
+ "error": error,
930
+ }
931
+
932
+ if not is_valid:
933
+ issues.append(f"{adapter_name}: {error}")
934
+
935
+ return {
936
+ "status": "completed",
937
+ "validation_results": results,
938
+ "all_valid": len(issues) == 0,
939
+ "issues": issues,
940
+ "message": (
941
+ "All configurations valid"
942
+ if len(issues) == 0
943
+ else f"Found {len(issues)} validation issue(s)"
944
+ ),
945
+ }
946
+ except Exception as e:
947
+ return {
948
+ "status": "error",
949
+ "error": f"Failed to validate configuration: {str(e)}",
950
+ }
951
+
952
+
953
+ async def config_test_adapter(adapter_name: str) -> dict[str, Any]:
954
+ """Test connectivity for a specific adapter (actual API call).
955
+
956
+ .. deprecated::
957
+ Use config(action="test", adapter_name="...") instead.
958
+ This tool will be removed in a future version.
959
+
960
+ Args: adapter_name - Adapter to test (linear, github, jira, aitrackdown)
961
+ Returns: ValidationResponse with adapter, healthy status, message, error_type
962
+ Note: Makes real API call (list operation) to verify credentials and connectivity
963
+ See: docs/mcp-api-reference.md#validation-response-format
964
+ """
965
+ warnings.warn(
966
+ "config_test_adapter is deprecated. Use config(action='test', adapter_name=adapter_name) instead.",
967
+ DeprecationWarning,
968
+ stacklevel=2,
969
+ )
970
+ try:
971
+ # Import diagnostic tool
972
+ from .diagnostic_tools import check_adapter_health
973
+
974
+ # Validate adapter name
975
+ valid_adapters = [adapter_type.value for adapter_type in AdapterType]
976
+ if adapter_name.lower() not in valid_adapters:
977
+ return {
978
+ "status": "error",
979
+ "error": f"Invalid adapter '{adapter_name}'",
980
+ "valid_adapters": valid_adapters,
981
+ }
982
+
983
+ # Use existing health check infrastructure
984
+ result = await check_adapter_health(adapter_name=adapter_name)
985
+
986
+ if result["status"] == "error":
987
+ return result
988
+
989
+ # Extract adapter-specific result
990
+ adapter_result = result["adapters"][adapter_name]
991
+
992
+ return {
993
+ "status": "completed",
994
+ "adapter": adapter_name,
995
+ "healthy": adapter_result["status"] == "healthy",
996
+ "message": adapter_result.get("message") or adapter_result.get("error"),
997
+ "error_type": adapter_result.get("error_type"),
998
+ }
999
+ except Exception as e:
1000
+ return {
1001
+ "status": "error",
1002
+ "error": f"Failed to test adapter: {str(e)}",
1003
+ }
1004
+
1005
+
1006
+ async def config_list_adapters() -> dict[str, Any]:
1007
+ """List all available adapters with configuration status.
1008
+
1009
+ .. deprecated::
1010
+ Use config(action="list_adapters") instead.
1011
+ This tool will be removed in a future version.
1012
+
1013
+ Returns: ListResponse with adapters array (type, name, configured, is_default, description), default_adapter, total_configured
1014
+ See: docs/mcp-api-reference.md#list-response-format
1015
+ docs/mcp-api-reference.md#adapter-types
1016
+ """
1017
+ warnings.warn(
1018
+ "config_list_adapters is deprecated. Use config(action='list_adapters') instead.",
1019
+ DeprecationWarning,
1020
+ stacklevel=2,
1021
+ )
1022
+ try:
1023
+ # Get all registered adapters from registry
1024
+ available_adapters = AdapterRegistry.list_adapters()
1025
+
1026
+ # Load project config to check which are configured
1027
+ resolver = get_resolver()
1028
+ config = resolver.load_project_config() or TicketerConfig()
1029
+
1030
+ # Map of adapter type to human-readable descriptions
1031
+ adapter_descriptions = {
1032
+ "linear": "Linear issue tracking",
1033
+ "github": "GitHub Issues",
1034
+ "jira": "Atlassian JIRA",
1035
+ "aitrackdown": "File-based ticket tracking",
1036
+ "asana": "Asana project management",
1037
+ }
1038
+
1039
+ # Build adapter list with status
1040
+ adapters = []
1041
+ for adapter_type, _adapter_class in available_adapters.items():
1042
+ # Check if this adapter is configured
1043
+ is_configured = adapter_type in config.adapters
1044
+ is_default = config.default_adapter == adapter_type
1045
+
1046
+ # Get display name from adapter class
1047
+ # Create temporary instance to get display name
1048
+ try:
1049
+ # Use adapter_type.title() as fallback for display name
1050
+ display_name = adapter_type.title()
1051
+ except Exception:
1052
+ display_name = adapter_type.title()
1053
+
1054
+ adapters.append(
1055
+ {
1056
+ "type": adapter_type,
1057
+ "name": display_name,
1058
+ "configured": is_configured,
1059
+ "is_default": is_default,
1060
+ "description": adapter_descriptions.get(
1061
+ adapter_type, f"{display_name} adapter"
1062
+ ),
1063
+ }
1064
+ )
1065
+
1066
+ # Sort adapters: configured first, then by name
1067
+ adapters.sort(key=lambda x: (not x["configured"], x["type"]))
1068
+
1069
+ total_configured = sum(1 for a in adapters if a["configured"])
1070
+
1071
+ return {
1072
+ "status": "completed",
1073
+ "adapters": adapters,
1074
+ "default_adapter": config.default_adapter,
1075
+ "total_configured": total_configured,
1076
+ "message": (
1077
+ f"{total_configured} adapter(s) configured"
1078
+ if total_configured > 0
1079
+ else "No adapters configured"
1080
+ ),
1081
+ }
1082
+ except Exception as e:
1083
+ return {
1084
+ "status": "error",
1085
+ "error": f"Failed to list adapters: {str(e)}",
1086
+ }
1087
+
1088
+
1089
+ async def config_get_adapter_requirements(adapter: str) -> dict[str, Any]:
1090
+ """Get configuration requirements for a specific adapter.
1091
+
1092
+ .. deprecated::
1093
+ Use config(action="get_requirements", adapter="...") instead.
1094
+ This tool will be removed in a future version.
1095
+
1096
+ Args: adapter - Adapter name (linear, github, jira, aitrackdown, asana)
1097
+ Returns: Requirements dict with field specs (type, required, description, env_var, validation pattern)
1098
+ See: docs/mcp-api-reference.md#adapter-types for setup instructions
1099
+ """
1100
+ warnings.warn(
1101
+ "config_get_adapter_requirements is deprecated. Use config(action='get_requirements', adapter=adapter) instead.",
1102
+ DeprecationWarning,
1103
+ stacklevel=2,
1104
+ )
1105
+ try:
1106
+ # Validate adapter name
1107
+ valid_adapters = [adapter_type.value for adapter_type in AdapterType]
1108
+ if adapter.lower() not in valid_adapters:
1109
+ return {
1110
+ "status": "error",
1111
+ "error": f"Invalid adapter '{adapter}'. Must be one of: {', '.join(valid_adapters)}",
1112
+ "valid_adapters": valid_adapters,
1113
+ }
1114
+
1115
+ adapter_type = adapter.lower()
1116
+
1117
+ # Define requirements for each adapter based on ConfigValidator logic
1118
+ requirements_map = {
1119
+ "linear": {
1120
+ "api_key": {
1121
+ "type": "string",
1122
+ "required": True,
1123
+ "description": "Linear API key (get from Linear Settings > API)",
1124
+ "env_var": "LINEAR_API_KEY",
1125
+ "validation": "^lin_api_[a-zA-Z0-9]{40}$",
1126
+ },
1127
+ "team_key": {
1128
+ "type": "string",
1129
+ "required": True,
1130
+ "description": "Team key (e.g., 'ENG') OR team_id (UUID). At least one required.",
1131
+ "env_var": "LINEAR_TEAM_KEY",
1132
+ },
1133
+ "team_id": {
1134
+ "type": "string",
1135
+ "required": False,
1136
+ "description": "Team UUID (alternative to team_key)",
1137
+ "env_var": "LINEAR_TEAM_ID",
1138
+ "validation": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
1139
+ },
1140
+ "workspace": {
1141
+ "type": "string",
1142
+ "required": False,
1143
+ "description": "Linear workspace name (for documentation only)",
1144
+ "env_var": "LINEAR_WORKSPACE",
1145
+ },
1146
+ },
1147
+ "github": {
1148
+ "token": {
1149
+ "type": "string",
1150
+ "required": True,
1151
+ "description": "GitHub personal access token (or api_key alias)",
1152
+ "env_var": "GITHUB_TOKEN",
1153
+ },
1154
+ "owner": {
1155
+ "type": "string",
1156
+ "required": True,
1157
+ "description": "Repository owner (username or organization)",
1158
+ "env_var": "GITHUB_OWNER",
1159
+ },
1160
+ "repo": {
1161
+ "type": "string",
1162
+ "required": True,
1163
+ "description": "Repository name",
1164
+ "env_var": "GITHUB_REPO",
1165
+ },
1166
+ },
1167
+ "jira": {
1168
+ "server": {
1169
+ "type": "string",
1170
+ "required": True,
1171
+ "description": "JIRA server URL (e.g., https://company.atlassian.net)",
1172
+ "env_var": "JIRA_SERVER",
1173
+ "validation": "^https?://",
1174
+ },
1175
+ "email": {
1176
+ "type": "string",
1177
+ "required": True,
1178
+ "description": "JIRA account email address",
1179
+ "env_var": "JIRA_EMAIL",
1180
+ "validation": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
1181
+ },
1182
+ "api_token": {
1183
+ "type": "string",
1184
+ "required": True,
1185
+ "description": "JIRA API token (get from Atlassian Account Settings)",
1186
+ "env_var": "JIRA_API_TOKEN",
1187
+ },
1188
+ "project_key": {
1189
+ "type": "string",
1190
+ "required": False,
1191
+ "description": "Default JIRA project key (e.g., 'PROJ')",
1192
+ "env_var": "JIRA_PROJECT_KEY",
1193
+ },
1194
+ },
1195
+ "aitrackdown": {
1196
+ "base_path": {
1197
+ "type": "string",
1198
+ "required": False,
1199
+ "description": "Base directory for ticket storage (defaults to .aitrackdown)",
1200
+ "env_var": "AITRACKDOWN_BASE_PATH",
1201
+ },
1202
+ },
1203
+ "asana": {
1204
+ "api_key": {
1205
+ "type": "string",
1206
+ "required": True,
1207
+ "description": "Asana Personal Access Token",
1208
+ "env_var": "ASANA_API_KEY",
1209
+ },
1210
+ "workspace": {
1211
+ "type": "string",
1212
+ "required": False,
1213
+ "description": "Asana workspace GID (optional, can be auto-detected)",
1214
+ "env_var": "ASANA_WORKSPACE",
1215
+ },
1216
+ },
1217
+ }
1218
+
1219
+ requirements = requirements_map.get(adapter_type, {})
1220
+
1221
+ return {
1222
+ "status": "completed",
1223
+ "adapter": adapter_type,
1224
+ "requirements": requirements,
1225
+ "total_fields": len(requirements),
1226
+ "required_fields": [
1227
+ field for field, spec in requirements.items() if spec.get("required")
1228
+ ],
1229
+ "optional_fields": [
1230
+ field
1231
+ for field, spec in requirements.items()
1232
+ if not spec.get("required")
1233
+ ],
1234
+ }
1235
+ except Exception as e:
1236
+ return {
1237
+ "status": "error",
1238
+ "error": f"Failed to get adapter requirements: {str(e)}",
1239
+ }
1240
+
1241
+
1242
+ async def config_setup_wizard(
1243
+ adapter_type: str,
1244
+ credentials: dict[str, Any],
1245
+ set_as_default: bool = True,
1246
+ test_connection: bool = True,
1247
+ ) -> dict[str, Any]:
1248
+ """Interactive setup wizard for adapter configuration (validates, tests, saves).
1249
+
1250
+ .. deprecated::
1251
+ Use config(action="setup_wizard", adapter_type="...", credentials={...}) instead.
1252
+ This function will be removed in a future version.
1253
+
1254
+ Args: adapter_type, credentials dict, set_as_default (default: True), test_connection (default: True)
1255
+ Returns: ConfigResponse with adapter, message, tested, connection_healthy, config_path
1256
+ Note: Single-call setup - validates format, tests API connectivity, saves config
1257
+ See: docs/mcp-api-reference.md#config-response-format
1258
+ docs/mcp-api-reference.md#adapter-types
1259
+ """
1260
+ warnings.warn(
1261
+ "config_setup_wizard is deprecated. Use config(action='setup_wizard', adapter_type=adapter_type, credentials=credentials) instead.",
1262
+ DeprecationWarning,
1263
+ stacklevel=2,
1264
+ )
1265
+ try:
1266
+ # Step 1: Validate adapter type
1267
+ valid_adapters = [adapter_type.value for adapter_type in AdapterType]
1268
+ adapter_lower = adapter_type.lower()
1269
+
1270
+ if adapter_lower not in valid_adapters:
1271
+ return {
1272
+ "status": "error",
1273
+ "error": f"Invalid adapter '{adapter_type}'. Must be one of: {', '.join(valid_adapters)}",
1274
+ "valid_adapters": valid_adapters,
1275
+ }
1276
+
1277
+ # Step 2: Get adapter requirements
1278
+ requirements_result = await config_get_adapter_requirements(adapter_lower)
1279
+ if requirements_result["status"] == "error":
1280
+ return requirements_result
1281
+
1282
+ requirements = requirements_result["requirements"]
1283
+
1284
+ # Step 3: Validate credentials structure
1285
+ missing_fields = []
1286
+ invalid_fields = []
1287
+
1288
+ # Check for required fields
1289
+ for field_name, field_spec in requirements.items():
1290
+ if field_spec.get("required"):
1291
+ # Check if field is present and non-empty
1292
+ if field_name not in credentials or not credentials.get(field_name):
1293
+ # For Linear, check if either team_key or team_id is provided
1294
+ if adapter_lower == "linear" and field_name in [
1295
+ "team_key",
1296
+ "team_id",
1297
+ ]:
1298
+ # Special handling: either team_key OR team_id is required
1299
+ has_team_key = (
1300
+ credentials.get("team_key")
1301
+ and str(credentials["team_key"]).strip()
1302
+ )
1303
+ has_team_id = (
1304
+ credentials.get("team_id")
1305
+ and str(credentials["team_id"]).strip()
1306
+ )
1307
+ if not has_team_key and not has_team_id:
1308
+ missing_fields.append(
1309
+ "team_key OR team_id (at least one required)"
1310
+ )
1311
+ # If one is provided, we're good - don't add to missing_fields
1312
+ else:
1313
+ missing_fields.append(field_name)
1314
+
1315
+ if missing_fields:
1316
+ return {
1317
+ "status": "error",
1318
+ "error": f"Missing required credentials: {', '.join(missing_fields)}",
1319
+ "missing_fields": missing_fields,
1320
+ "required_fields": requirements_result["required_fields"],
1321
+ "hint": "Use config_get_adapter_requirements() to see all required fields",
1322
+ }
1323
+
1324
+ # Step 4: Validate credential formats
1325
+ import re
1326
+
1327
+ for field_name, field_value in credentials.items():
1328
+ if field_name not in requirements:
1329
+ continue
1330
+
1331
+ field_spec = requirements[field_name]
1332
+ validation_pattern = field_spec.get("validation")
1333
+
1334
+ if validation_pattern and field_value:
1335
+ try:
1336
+ if not re.match(validation_pattern, str(field_value)):
1337
+ invalid_fields.append(
1338
+ {
1339
+ "field": field_name,
1340
+ "error": f"Invalid format for {field_name}",
1341
+ "pattern": validation_pattern,
1342
+ "description": field_spec.get("description", ""),
1343
+ }
1344
+ )
1345
+ except Exception as e:
1346
+ # If regex fails, log but continue (don't block on validation)
1347
+ import logging
1348
+
1349
+ logger = logging.getLogger(__name__)
1350
+ logger.warning(f"Validation pattern error for {field_name}: {e}")
1351
+
1352
+ if invalid_fields:
1353
+ return {
1354
+ "status": "error",
1355
+ "error": f"Invalid credential format for: {', '.join(f['field'] for f in invalid_fields)}",
1356
+ "invalid_fields": invalid_fields,
1357
+ }
1358
+
1359
+ # Step 5: Build adapter config
1360
+ from ....core.project_config import AdapterConfig
1361
+
1362
+ adapter_config = AdapterConfig(adapter=adapter_lower, **credentials)
1363
+
1364
+ # Step 6: Validate using ConfigValidator
1365
+ is_valid, validation_error = ConfigValidator.validate(
1366
+ adapter_lower, adapter_config.to_dict()
1367
+ )
1368
+
1369
+ if not is_valid:
1370
+ return {
1371
+ "status": "error",
1372
+ "error": f"Configuration validation failed: {validation_error}",
1373
+ "validation_error": validation_error,
1374
+ }
1375
+
1376
+ # Step 7: Test connection if enabled
1377
+ connection_healthy = None
1378
+ test_error = None
1379
+
1380
+ if test_connection:
1381
+ # Save config temporarily for testing (preserves adapters)
1382
+ config = _safe_load_config()
1383
+ config.adapters[adapter_lower] = adapter_config
1384
+
1385
+ resolver = get_resolver()
1386
+ resolver.save_project_config(config)
1387
+
1388
+ # Test the adapter with enhanced error handling (1M-431)
1389
+ import logging
1390
+
1391
+ logger = logging.getLogger(__name__)
1392
+
1393
+ try:
1394
+ test_result = await config_test_adapter(adapter_lower)
1395
+
1396
+ if test_result["status"] == "error":
1397
+ logger.error(
1398
+ f"Connection test failed for {adapter_lower}: {test_result.get('error')}"
1399
+ )
1400
+ return {
1401
+ "status": "error",
1402
+ "error": f"Connection test failed: {test_result.get('error')}",
1403
+ "test_result": test_result,
1404
+ "message": "Configuration was saved but connection test failed.",
1405
+ "troubleshooting": [
1406
+ "1. Verify API key is correct and starts with expected prefix",
1407
+ f"2. Check network connectivity to {adapter_lower} API",
1408
+ "3. Ensure credentials have proper permissions",
1409
+ "4. Review application logs for detailed error information",
1410
+ "5. Try running config_test_adapter() separately for more details",
1411
+ ],
1412
+ }
1413
+
1414
+ connection_healthy = test_result.get("healthy", False)
1415
+
1416
+ if not connection_healthy:
1417
+ test_error = test_result.get("message", "Unknown connection error")
1418
+ logger.warning(
1419
+ f"Connection test unhealthy for {adapter_lower}: {test_error}"
1420
+ )
1421
+ return {
1422
+ "status": "error",
1423
+ "error": f"Connection test failed: {test_error}",
1424
+ "test_result": test_result,
1425
+ "message": "Configuration was saved but adapter could not connect.",
1426
+ "troubleshooting": [
1427
+ "1. Check adapter logs for specific error details",
1428
+ "2. Verify API permissions in service settings",
1429
+ "3. Ensure all required configuration fields are provided",
1430
+ "4. Test credentials directly via service web interface",
1431
+ ],
1432
+ }
1433
+
1434
+ except Exception as e:
1435
+ logger.error(
1436
+ f"Connection test exception for {adapter_lower}: {type(e).__name__}: {e}",
1437
+ exc_info=True,
1438
+ )
1439
+ return {
1440
+ "status": "error",
1441
+ "error": f"Connection test failed with exception: {type(e).__name__}: {e}",
1442
+ "message": "Configuration was saved but connection test raised an exception.",
1443
+ "troubleshooting": [
1444
+ "1. This may indicate a code bug rather than configuration issue",
1445
+ "2. Check application logs for full stack trace",
1446
+ "3. Verify all required dependencies are installed",
1447
+ "4. Report to maintainers if issue persists",
1448
+ ],
1449
+ }
1450
+ else:
1451
+ # Save config without testing (preserves adapters)
1452
+ config = _safe_load_config()
1453
+ config.adapters[adapter_lower] = adapter_config
1454
+
1455
+ resolver = get_resolver()
1456
+ resolver.save_project_config(config)
1457
+
1458
+ # Step 8: Set as default if enabled
1459
+ if set_as_default:
1460
+ # Update default adapter (preserves adapters)
1461
+ config = _safe_load_config()
1462
+ config.default_adapter = adapter_lower
1463
+
1464
+ resolver = get_resolver()
1465
+ resolver.save_project_config(config)
1466
+
1467
+ # Step 9: Return success
1468
+ config_path = resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH
1469
+
1470
+ return {
1471
+ "status": "completed",
1472
+ "adapter": adapter_lower,
1473
+ "message": f"{adapter_lower.title()} adapter configured successfully",
1474
+ "tested": test_connection,
1475
+ "connection_healthy": connection_healthy if test_connection else None,
1476
+ "set_as_default": set_as_default,
1477
+ "config_path": str(config_path),
1478
+ }
1479
+
1480
+ except Exception as e:
1481
+ import traceback
1482
+
1483
+ return {
1484
+ "status": "error",
1485
+ "error": f"Setup wizard failed: {str(e)}",
1486
+ "traceback": traceback.format_exc(),
1487
+ }
1488
+
1489
+
1490
+ async def config_set_project_from_url(
1491
+ project_url: str,
1492
+ test_connection: bool = True,
1493
+ ) -> dict[str, Any]:
1494
+ """Set default project from URL with comprehensive validation.
1495
+
1496
+ This function provides enhanced project URL handling:
1497
+ 1. Parses project URL to detect platform
1498
+ 2. Validates adapter configuration and credentials
1499
+ 3. Optionally tests project accessibility
1500
+ 4. Sets as default project if all validations pass
1501
+
1502
+ Args:
1503
+ project_url: Project URL from any supported platform
1504
+ test_connection: Test project accessibility (default: True)
1505
+
1506
+ Returns:
1507
+ ConfigResponse with status, platform, project_id, validation details
1508
+
1509
+ Examples:
1510
+ # Set Linear project with validation
1511
+ config_set_project_from_url("https://linear.app/team/project/abc-123")
1512
+
1513
+ # Set GitHub project without connectivity test
1514
+ config_set_project_from_url("https://github.com/owner/repo/projects/1", test_connection=False)
1515
+
1516
+ Error Scenarios:
1517
+ - Invalid URL format: Returns parsing error with format examples
1518
+ - Adapter not configured: Returns setup instructions for platform
1519
+ - Invalid credentials: Returns credential validation errors
1520
+ - Project not accessible: Returns accessibility error with troubleshooting
1521
+
1522
+ """
1523
+ try:
1524
+ # Import project validator
1525
+ from ....core.project_validator import ProjectValidator
1526
+
1527
+ # Create validator
1528
+ validator = ProjectValidator(project_path=Path.cwd())
1529
+
1530
+ # Validate project URL
1531
+ result = validator.validate_project_url(
1532
+ url=project_url, test_connection=test_connection
1533
+ )
1534
+
1535
+ # Check validation result
1536
+ if not result.valid:
1537
+ return {
1538
+ "status": "error",
1539
+ "error": result.error,
1540
+ "error_type": result.error_type,
1541
+ "platform": result.platform,
1542
+ "project_id": result.project_id,
1543
+ "adapter_configured": result.adapter_configured,
1544
+ "adapter_valid": result.adapter_valid,
1545
+ "suggestions": result.suggestions,
1546
+ "credential_errors": result.credential_errors,
1547
+ "adapter_config": result.adapter_config,
1548
+ }
1549
+
1550
+ # Validation passed - set as default project
1551
+ project_id = result.project_id
1552
+ platform = result.platform
1553
+
1554
+ # Load current configuration safely (preserves adapters)
1555
+ config = _safe_load_config()
1556
+
1557
+ # Store previous project for response
1558
+ previous_project = config.default_project or config.default_epic
1559
+
1560
+ # Update default project (and epic for backward compat)
1561
+ config.default_project = project_id
1562
+ config.default_epic = project_id
1563
+
1564
+ # Also update default adapter to match the project's platform
1565
+ previous_adapter = config.default_adapter
1566
+ config.default_adapter = platform
1567
+
1568
+ # Save configuration
1569
+ resolver = get_resolver()
1570
+ resolver.save_project_config(config)
1571
+
1572
+ return {
1573
+ "status": "completed",
1574
+ "message": f"Default project set to '{project_id}' from {platform.title()}",
1575
+ "platform": platform,
1576
+ "project_id": project_id,
1577
+ "project_url": project_url,
1578
+ "previous_project": previous_project,
1579
+ "new_project": project_id,
1580
+ "adapter_changed": previous_adapter != platform,
1581
+ "previous_adapter": previous_adapter,
1582
+ "new_adapter": platform,
1583
+ "validated": True,
1584
+ "connection_tested": test_connection,
1585
+ "config_path": str(resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH),
1586
+ }
1587
+
1588
+ except Exception as e:
1589
+ import traceback
1590
+
1591
+ logger.error(f"Failed to set project from URL: {e}", exc_info=True)
1592
+ return {
1593
+ "status": "error",
1594
+ "error": f"Failed to set project from URL: {str(e)}",
1595
+ "traceback": traceback.format_exc(),
1596
+ }
1597
+
1598
+
353
1599
  def _mask_sensitive_values(config: dict[str, Any]) -> dict[str, Any]:
354
1600
  """Mask sensitive values in configuration dictionary.
355
1601