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.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +394 -9
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1416 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +348 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +836 -105
- mcp_ticketer/adapters/hybrid.py +47 -5
- mcp_ticketer/adapters/jira.py +772 -1
- mcp_ticketer/adapters/linear/adapter.py +2293 -108
- mcp_ticketer/adapters/linear/client.py +146 -12
- mcp_ticketer/adapters/linear/mappers.py +105 -11
- mcp_ticketer/adapters/linear/queries.py +168 -1
- mcp_ticketer/adapters/linear/types.py +80 -4
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -0
- mcp_ticketer/cache/memory.py +3 -3
- mcp_ticketer/cli/adapter_diagnostics.py +4 -2
- mcp_ticketer/cli/auggie_configure.py +18 -6
- mcp_ticketer/cli/codex_configure.py +175 -60
- mcp_ticketer/cli/configure.py +884 -146
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +31 -28
- mcp_ticketer/cli/discover.py +293 -21
- mcp_ticketer/cli/gemini_configure.py +18 -6
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +99 -15
- mcp_ticketer/cli/main.py +109 -2055
- mcp_ticketer/cli/mcp_configure.py +673 -99
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +6 -6
- mcp_ticketer/cli/platform_detection.py +477 -0
- mcp_ticketer/cli/platform_installer.py +536 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +13 -11
- mcp_ticketer/cli/ticket_commands.py +277 -36
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +45 -41
- mcp_ticketer/core/__init__.py +35 -1
- mcp_ticketer/core/adapter.py +170 -5
- mcp_ticketer/core/config.py +38 -31
- mcp_ticketer/core/env_discovery.py +33 -3
- mcp_ticketer/core/env_loader.py +7 -6
- mcp_ticketer/core/exceptions.py +10 -4
- mcp_ticketer/core/http_client.py +10 -10
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +32 -20
- mcp_ticketer/core/models.py +136 -1
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +148 -14
- mcp_ticketer/core/registry.py +1 -1
- mcp_ticketer/core/session_state.py +171 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +2 -2
- mcp_ticketer/mcp/server/__init__.py +2 -2
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/main.py +187 -93
- mcp_ticketer/mcp/server/routing.py +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +58 -0
- mcp_ticketer/mcp/server/tools/__init__.py +37 -9
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +65 -20
- mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
- mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
- mcp_ticketer/mcp/server/tools/config_tools.py +1429 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +878 -319
- mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1182 -82
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
- mcp_ticketer/queue/health_monitor.py +1 -0
- mcp_ticketer/queue/manager.py +4 -4
- mcp_ticketer/queue/queue.py +3 -3
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +2 -2
- mcp_ticketer/queue/worker.py +15 -13
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
- mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
- mcp_ticketer-0.4.11.dist-info/METADATA +0 -496
- mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {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
|