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