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