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