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