mcp-ticketer 0.4.11__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 (111) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/adapters/__init__.py +2 -0
  4. mcp_ticketer/adapters/aitrackdown.py +394 -9
  5. mcp_ticketer/adapters/asana/__init__.py +15 -0
  6. mcp_ticketer/adapters/asana/adapter.py +1416 -0
  7. mcp_ticketer/adapters/asana/client.py +292 -0
  8. mcp_ticketer/adapters/asana/mappers.py +348 -0
  9. mcp_ticketer/adapters/asana/types.py +146 -0
  10. mcp_ticketer/adapters/github.py +836 -105
  11. mcp_ticketer/adapters/hybrid.py +47 -5
  12. mcp_ticketer/adapters/jira.py +772 -1
  13. mcp_ticketer/adapters/linear/adapter.py +2293 -108
  14. mcp_ticketer/adapters/linear/client.py +146 -12
  15. mcp_ticketer/adapters/linear/mappers.py +105 -11
  16. mcp_ticketer/adapters/linear/queries.py +168 -1
  17. mcp_ticketer/adapters/linear/types.py +80 -4
  18. mcp_ticketer/analysis/__init__.py +56 -0
  19. mcp_ticketer/analysis/dependency_graph.py +255 -0
  20. mcp_ticketer/analysis/health_assessment.py +304 -0
  21. mcp_ticketer/analysis/orphaned.py +218 -0
  22. mcp_ticketer/analysis/project_status.py +594 -0
  23. mcp_ticketer/analysis/similarity.py +224 -0
  24. mcp_ticketer/analysis/staleness.py +266 -0
  25. mcp_ticketer/automation/__init__.py +11 -0
  26. mcp_ticketer/automation/project_updates.py +378 -0
  27. mcp_ticketer/cache/memory.py +3 -3
  28. mcp_ticketer/cli/adapter_diagnostics.py +4 -2
  29. mcp_ticketer/cli/auggie_configure.py +18 -6
  30. mcp_ticketer/cli/codex_configure.py +175 -60
  31. mcp_ticketer/cli/configure.py +884 -146
  32. mcp_ticketer/cli/cursor_configure.py +314 -0
  33. mcp_ticketer/cli/diagnostics.py +31 -28
  34. mcp_ticketer/cli/discover.py +293 -21
  35. mcp_ticketer/cli/gemini_configure.py +18 -6
  36. mcp_ticketer/cli/init_command.py +880 -0
  37. mcp_ticketer/cli/instruction_commands.py +435 -0
  38. mcp_ticketer/cli/linear_commands.py +99 -15
  39. mcp_ticketer/cli/main.py +109 -2055
  40. mcp_ticketer/cli/mcp_configure.py +673 -99
  41. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  42. mcp_ticketer/cli/migrate_config.py +12 -8
  43. mcp_ticketer/cli/platform_commands.py +6 -6
  44. mcp_ticketer/cli/platform_detection.py +477 -0
  45. mcp_ticketer/cli/platform_installer.py +536 -0
  46. mcp_ticketer/cli/project_update_commands.py +350 -0
  47. mcp_ticketer/cli/queue_commands.py +15 -15
  48. mcp_ticketer/cli/setup_command.py +639 -0
  49. mcp_ticketer/cli/simple_health.py +13 -11
  50. mcp_ticketer/cli/ticket_commands.py +277 -36
  51. mcp_ticketer/cli/update_checker.py +313 -0
  52. mcp_ticketer/cli/utils.py +45 -41
  53. mcp_ticketer/core/__init__.py +35 -1
  54. mcp_ticketer/core/adapter.py +170 -5
  55. mcp_ticketer/core/config.py +38 -31
  56. mcp_ticketer/core/env_discovery.py +33 -3
  57. mcp_ticketer/core/env_loader.py +7 -6
  58. mcp_ticketer/core/exceptions.py +10 -4
  59. mcp_ticketer/core/http_client.py +10 -10
  60. mcp_ticketer/core/instructions.py +405 -0
  61. mcp_ticketer/core/label_manager.py +732 -0
  62. mcp_ticketer/core/mappers.py +32 -20
  63. mcp_ticketer/core/models.py +136 -1
  64. mcp_ticketer/core/onepassword_secrets.py +379 -0
  65. mcp_ticketer/core/priority_matcher.py +463 -0
  66. mcp_ticketer/core/project_config.py +148 -14
  67. mcp_ticketer/core/registry.py +1 -1
  68. mcp_ticketer/core/session_state.py +171 -0
  69. mcp_ticketer/core/state_matcher.py +592 -0
  70. mcp_ticketer/core/url_parser.py +425 -0
  71. mcp_ticketer/core/validators.py +69 -0
  72. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  73. mcp_ticketer/mcp/__init__.py +2 -2
  74. mcp_ticketer/mcp/server/__init__.py +2 -2
  75. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  76. mcp_ticketer/mcp/server/main.py +187 -93
  77. mcp_ticketer/mcp/server/routing.py +655 -0
  78. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  79. mcp_ticketer/mcp/server/tools/__init__.py +37 -9
  80. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  81. mcp_ticketer/mcp/server/tools/attachment_tools.py +65 -20
  82. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  83. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  84. mcp_ticketer/mcp/server/tools/config_tools.py +1429 -0
  85. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  86. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +878 -319
  87. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  88. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  89. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  90. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  91. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  92. mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
  93. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  94. mcp_ticketer/mcp/server/tools/ticket_tools.py +1182 -82
  95. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  96. mcp_ticketer/queue/health_monitor.py +1 -0
  97. mcp_ticketer/queue/manager.py +4 -4
  98. mcp_ticketer/queue/queue.py +3 -3
  99. mcp_ticketer/queue/run_worker.py +1 -1
  100. mcp_ticketer/queue/ticket_registry.py +2 -2
  101. mcp_ticketer/queue/worker.py +15 -13
  102. mcp_ticketer/utils/__init__.py +5 -0
  103. mcp_ticketer/utils/token_utils.py +246 -0
  104. mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
  105. mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
  106. mcp_ticketer-0.4.11.dist-info/METADATA +0 -496
  107. mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
  108. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
  109. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
  110. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
  111. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1429 @@
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 warnings
30
+ from pathlib import Path
31
+ from typing import Any
32
+
33
+ from ....core.project_config import (
34
+ AdapterType,
35
+ ConfigResolver,
36
+ ConfigValidator,
37
+ TicketerConfig,
38
+ )
39
+ from ....core.registry import AdapterRegistry
40
+ from ..server_sdk import mcp
41
+
42
+
43
+ def get_resolver() -> ConfigResolver:
44
+ """Get or create the configuration resolver.
45
+
46
+ Returns:
47
+ ConfigResolver instance for current working directory
48
+
49
+ Design Decision: Uses CWD as project root, assuming MCP server
50
+ is started from project directory. This matches user expectations
51
+ and aligns with how other development tools operate.
52
+
53
+ Note: Creates a new resolver each time to avoid caching issues
54
+ in tests and ensure current working directory is always used.
55
+
56
+ """
57
+ return ConfigResolver(project_path=Path.cwd())
58
+
59
+
60
+ @mcp.tool()
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).
74
+
75
+ Single tool for all 16 configuration operations. Consolidates all config_*
76
+ tools into one interface for ~7,200 token savings (90% reduction).
77
+
78
+ Args:
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
97
+
98
+ Returns:
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",
332
+ }
333
+
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
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
+ )
352
+ try:
353
+ # Validate adapter name against registry
354
+ valid_adapters = [adapter_type.value for adapter_type in AdapterType]
355
+ if adapter.lower() not in valid_adapters:
356
+ return {
357
+ "status": "error",
358
+ "error": f"Invalid adapter '{adapter}'. Must be one of: {', '.join(valid_adapters)}",
359
+ "valid_adapters": valid_adapters,
360
+ }
361
+
362
+ # Load current configuration
363
+ resolver = get_resolver()
364
+ config = resolver.load_project_config() or TicketerConfig()
365
+
366
+ # Store previous adapter for response
367
+ previous_adapter = config.default_adapter
368
+
369
+ # Update default adapter
370
+ config.default_adapter = adapter.lower()
371
+
372
+ # Save configuration
373
+ resolver.save_project_config(config)
374
+
375
+ return {
376
+ "status": "completed",
377
+ "message": f"Default adapter set to '{adapter.lower()}'",
378
+ "previous_adapter": previous_adapter,
379
+ "new_adapter": adapter.lower(),
380
+ "config_path": str(resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH),
381
+ }
382
+ except Exception as e:
383
+ return {
384
+ "status": "error",
385
+ "error": f"Failed to set default adapter: {str(e)}",
386
+ }
387
+
388
+
389
+ async def config_set_default_project(
390
+ project_id: str,
391
+ project_key: str | None = None,
392
+ ) -> dict[str, Any]:
393
+ """Set the default project/epic for new tickets.
394
+
395
+ .. deprecated::
396
+ Use config_set(key="project", value="project_id") instead.
397
+ This tool will be removed in a future version.
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
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
+ )
409
+ try:
410
+ # Load current configuration
411
+ resolver = get_resolver()
412
+ config = resolver.load_project_config() or TicketerConfig()
413
+
414
+ # Store previous project for response
415
+ previous_project = config.default_project or config.default_epic
416
+
417
+ # Update default project (and epic for backward compat)
418
+ config.default_project = project_id if project_id else None
419
+ config.default_epic = project_id if project_id else None
420
+
421
+ # Save configuration
422
+ resolver.save_project_config(config)
423
+
424
+ return {
425
+ "status": "completed",
426
+ "message": (
427
+ f"Default project set to '{project_id}'"
428
+ if project_id
429
+ else "Default project cleared"
430
+ ),
431
+ "previous_project": previous_project,
432
+ "new_project": project_id,
433
+ "config_path": str(resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH),
434
+ }
435
+ except Exception as e:
436
+ return {
437
+ "status": "error",
438
+ "error": f"Failed to set default project: {str(e)}",
439
+ }
440
+
441
+
442
+ async def config_set_default_user(
443
+ user_id: str,
444
+ user_email: str | None = None,
445
+ ) -> dict[str, Any]:
446
+ """Set the default assignee for new tickets.
447
+
448
+ .. deprecated::
449
+ Use config_set(key="user", value="user_id") instead.
450
+ This tool will be removed in a future version.
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
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
+ )
461
+ try:
462
+ # Load current configuration
463
+ resolver = get_resolver()
464
+ config = resolver.load_project_config() or TicketerConfig()
465
+
466
+ # Store previous user for response
467
+ previous_user = config.default_user
468
+
469
+ # Update default user
470
+ config.default_user = user_id if user_id else None
471
+
472
+ # Save configuration
473
+ resolver.save_project_config(config)
474
+
475
+ return {
476
+ "status": "completed",
477
+ "message": (
478
+ f"Default user set to '{user_id}'"
479
+ if user_id
480
+ else "Default user cleared"
481
+ ),
482
+ "previous_user": previous_user,
483
+ "new_user": user_id,
484
+ "config_path": str(resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH),
485
+ }
486
+ except Exception as e:
487
+ return {
488
+ "status": "error",
489
+ "error": f"Failed to set default user: {str(e)}",
490
+ }
491
+
492
+
493
+ async def config_get() -> dict[str, Any]:
494
+ """Get current configuration settings.
495
+
496
+ .. deprecated::
497
+ Use config(action="get") instead.
498
+ This tool will be removed in a future version.
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
503
+ """
504
+ warnings.warn(
505
+ "config_get is deprecated. Use config(action='get') instead.",
506
+ DeprecationWarning,
507
+ stacklevel=2,
508
+ )
509
+ try:
510
+ # Load current configuration
511
+ resolver = get_resolver()
512
+ config = resolver.load_project_config() or TicketerConfig()
513
+
514
+ # Convert to dictionary
515
+ config_dict = config.to_dict()
516
+
517
+ # Mask sensitive values (API keys, tokens)
518
+ masked_config = _mask_sensitive_values(config_dict)
519
+
520
+ config_path = resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH
521
+ config_exists = config_path.exists()
522
+
523
+ return {
524
+ "status": "completed",
525
+ "config": masked_config,
526
+ "config_path": str(config_path),
527
+ "config_exists": config_exists,
528
+ "message": (
529
+ "Configuration retrieved successfully"
530
+ if config_exists
531
+ else "No configuration file found, showing defaults"
532
+ ),
533
+ }
534
+ except Exception as e:
535
+ return {
536
+ "status": "error",
537
+ "error": f"Failed to retrieve configuration: {str(e)}",
538
+ }
539
+
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
+
1401
+ def _mask_sensitive_values(config: dict[str, Any]) -> dict[str, Any]:
1402
+ """Mask sensitive values in configuration dictionary.
1403
+
1404
+ Args:
1405
+ config: Configuration dictionary
1406
+
1407
+ Returns:
1408
+ Configuration dictionary with sensitive values masked
1409
+
1410
+ Implementation Details:
1411
+ - Recursively processes nested dictionaries
1412
+ - Masks any field containing: key, token, password, secret
1413
+ - Preserves structure for debugging while protecting credentials
1414
+
1415
+ """
1416
+ masked = {}
1417
+ sensitive_keys = {"api_key", "token", "password", "secret", "api_token"}
1418
+
1419
+ for key, value in config.items():
1420
+ if isinstance(value, dict):
1421
+ # Recursively mask nested dictionaries
1422
+ masked[key] = _mask_sensitive_values(value)
1423
+ elif any(sensitive in key.lower() for sensitive in sensitive_keys):
1424
+ # Mask sensitive values
1425
+ masked[key] = "***" if value else None
1426
+ else:
1427
+ masked[key] = value
1428
+
1429
+ return masked