mcp-ticketer 2.0.1__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/__version__.py +1 -1
- mcp_ticketer/_version_scm.py +1 -0
- mcp_ticketer/adapters/aitrackdown.py +122 -0
- mcp_ticketer/adapters/asana/adapter.py +121 -0
- mcp_ticketer/adapters/github/__init__.py +26 -0
- mcp_ticketer/adapters/{github.py → github/adapter.py} +1506 -365
- 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/jira/__init__.py +35 -0
- mcp_ticketer/adapters/{jira.py → jira/adapter.py} +250 -678
- 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 +1000 -92
- mcp_ticketer/adapters/linear/client.py +91 -1
- mcp_ticketer/adapters/linear/mappers.py +107 -0
- mcp_ticketer/adapters/linear/queries.py +112 -2
- mcp_ticketer/adapters/linear/types.py +50 -10
- mcp_ticketer/cli/configure.py +524 -89
- mcp_ticketer/cli/install_mcp_server.py +418 -0
- mcp_ticketer/cli/main.py +10 -0
- mcp_ticketer/cli/mcp_configure.py +177 -49
- mcp_ticketer/cli/platform_installer.py +9 -0
- mcp_ticketer/cli/setup_command.py +157 -1
- mcp_ticketer/cli/ticket_commands.py +443 -81
- mcp_ticketer/cli/utils.py +113 -0
- mcp_ticketer/core/__init__.py +28 -0
- mcp_ticketer/core/adapter.py +367 -1
- mcp_ticketer/core/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +345 -0
- mcp_ticketer/core/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/session_state.py +6 -1
- mcp_ticketer/core/state_matcher.py +36 -3
- mcp_ticketer/mcp/server/__main__.py +2 -1
- mcp_ticketer/mcp/server/routing.py +68 -0
- mcp_ticketer/mcp/server/tools/__init__.py +7 -4
- mcp_ticketer/mcp/server/tools/attachment_tools.py +3 -1
- mcp_ticketer/mcp/server/tools/config_tools.py +233 -35
- mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +30 -1
- mcp_ticketer/mcp/server/tools/ticket_tools.py +37 -1
- mcp_ticketer/queue/queue.py +68 -0
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/METADATA +33 -3
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/RECORD +72 -36
- 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-2.0.1.dist-info/top_level.txt +0 -1
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/licenses/LICENSE +0 -0
|
@@ -199,7 +199,6 @@ class SemanticStateMatcher:
|
|
|
199
199
|
"complete",
|
|
200
200
|
"finished",
|
|
201
201
|
"resolved",
|
|
202
|
-
"closed",
|
|
203
202
|
"done done",
|
|
204
203
|
"done-done",
|
|
205
204
|
"delivered",
|
|
@@ -271,17 +270,51 @@ class SemanticStateMatcher:
|
|
|
271
270
|
"""Initialize the semantic state matcher.
|
|
272
271
|
|
|
273
272
|
Creates reverse lookup dictionary for O(1) synonym matching.
|
|
273
|
+
Detects and logs duplicate synonyms across states.
|
|
274
274
|
"""
|
|
275
|
+
import logging
|
|
276
|
+
|
|
277
|
+
logger = logging.getLogger(__name__)
|
|
278
|
+
|
|
275
279
|
# Build reverse lookup: synonym -> (state, is_exact)
|
|
276
280
|
self._synonym_to_state: dict[str, tuple[TicketState, bool]] = {}
|
|
277
281
|
|
|
282
|
+
# Track duplicates for validation
|
|
283
|
+
duplicate_check: dict[str, list[TicketState]] = {}
|
|
284
|
+
|
|
278
285
|
for state in TicketState:
|
|
279
286
|
# Add exact state value
|
|
280
|
-
|
|
287
|
+
normalized_value = state.value.lower()
|
|
288
|
+
self._synonym_to_state[normalized_value] = (state, True)
|
|
289
|
+
|
|
290
|
+
if normalized_value not in duplicate_check:
|
|
291
|
+
duplicate_check[normalized_value] = []
|
|
292
|
+
duplicate_check[normalized_value].append(state)
|
|
281
293
|
|
|
282
294
|
# Add all synonyms
|
|
283
295
|
for synonym in self.STATE_SYNONYMS.get(state, []):
|
|
284
|
-
|
|
296
|
+
normalized_synonym = synonym.lower()
|
|
297
|
+
|
|
298
|
+
# Check for duplicates
|
|
299
|
+
if normalized_synonym in duplicate_check:
|
|
300
|
+
duplicate_check[normalized_synonym].append(state)
|
|
301
|
+
else:
|
|
302
|
+
duplicate_check[normalized_synonym] = [state]
|
|
303
|
+
|
|
304
|
+
self._synonym_to_state[normalized_synonym] = (state, False)
|
|
305
|
+
|
|
306
|
+
# Log warnings for any duplicates found (excluding expected state value duplicates)
|
|
307
|
+
for synonym, states in duplicate_check.items():
|
|
308
|
+
if len(states) > 1:
|
|
309
|
+
# Filter out duplicate state values (they're expected - exact match + synonym)
|
|
310
|
+
unique_states = list(set(states))
|
|
311
|
+
if len(unique_states) > 1:
|
|
312
|
+
logger.warning(
|
|
313
|
+
"Duplicate synonym '%s' found in multiple states: %s. "
|
|
314
|
+
"This may cause non-deterministic behavior.",
|
|
315
|
+
synonym,
|
|
316
|
+
", ".join(s.value for s in unique_states),
|
|
317
|
+
)
|
|
285
318
|
|
|
286
319
|
def match_state(
|
|
287
320
|
self,
|
|
@@ -43,7 +43,8 @@ def run_server() -> None:
|
|
|
43
43
|
os.chdir(project_path)
|
|
44
44
|
sys.stderr.write(f"[MCP Server] Working directory: {project_path}\n")
|
|
45
45
|
except OSError as e:
|
|
46
|
-
sys.stderr.write(f"
|
|
46
|
+
sys.stderr.write(f"Error: Could not change to project directory: {e}\n")
|
|
47
|
+
sys.exit(1)
|
|
47
48
|
|
|
48
49
|
# Run the async main function
|
|
49
50
|
try:
|
|
@@ -637,6 +637,74 @@ class TicketRouter:
|
|
|
637
637
|
f"Failed to route list_tasks_by_issue operation: {str(e)}"
|
|
638
638
|
) from e
|
|
639
639
|
|
|
640
|
+
async def validate_project_access(
|
|
641
|
+
self, project_url: str, test_connection: bool = True
|
|
642
|
+
) -> dict[str, Any]:
|
|
643
|
+
"""Validate project URL and test accessibility.
|
|
644
|
+
|
|
645
|
+
This method provides comprehensive validation for project URLs:
|
|
646
|
+
1. Parses URL to extract platform and project ID
|
|
647
|
+
2. Validates adapter configuration exists
|
|
648
|
+
3. Validates adapter credentials
|
|
649
|
+
4. Optionally tests project accessibility via API
|
|
650
|
+
|
|
651
|
+
Args:
|
|
652
|
+
project_url: Project URL to validate
|
|
653
|
+
test_connection: If True, test actual API connectivity (default: True)
|
|
654
|
+
|
|
655
|
+
Returns:
|
|
656
|
+
Validation result dictionary with:
|
|
657
|
+
- valid (bool): Whether validation passed
|
|
658
|
+
- platform (str): Detected platform
|
|
659
|
+
- project_id (str): Extracted project ID
|
|
660
|
+
- adapter_configured (bool): Whether adapter is configured
|
|
661
|
+
- error (str): Error message if validation failed
|
|
662
|
+
- suggestions (list): Suggested actions to resolve error
|
|
663
|
+
|
|
664
|
+
Examples:
|
|
665
|
+
>>> router = TicketRouter(...)
|
|
666
|
+
>>> result = await router.validate_project_access("https://linear.app/team/project/abc-123")
|
|
667
|
+
>>> if result["valid"]:
|
|
668
|
+
... print(f"Project {result['project_id']} is accessible")
|
|
669
|
+
... else:
|
|
670
|
+
... print(f"Error: {result['error']}")
|
|
671
|
+
|
|
672
|
+
"""
|
|
673
|
+
try:
|
|
674
|
+
# Import project validator
|
|
675
|
+
# Create validator (use router's config for consistency)
|
|
676
|
+
from pathlib import Path
|
|
677
|
+
|
|
678
|
+
from ...core.project_validator import ProjectValidator
|
|
679
|
+
|
|
680
|
+
validator = ProjectValidator(project_path=Path.cwd())
|
|
681
|
+
|
|
682
|
+
# Validate project URL
|
|
683
|
+
validation_result = validator.validate_project_url(
|
|
684
|
+
url=project_url, test_connection=test_connection
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
# Convert dataclass to dictionary
|
|
688
|
+
return {
|
|
689
|
+
"valid": validation_result.valid,
|
|
690
|
+
"platform": validation_result.platform,
|
|
691
|
+
"project_id": validation_result.project_id,
|
|
692
|
+
"adapter_configured": validation_result.adapter_configured,
|
|
693
|
+
"adapter_valid": validation_result.adapter_valid,
|
|
694
|
+
"error": validation_result.error,
|
|
695
|
+
"error_type": validation_result.error_type,
|
|
696
|
+
"suggestions": validation_result.suggestions,
|
|
697
|
+
"credential_errors": validation_result.credential_errors,
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
except Exception as e:
|
|
701
|
+
logger.error(f"Project validation failed: {e}")
|
|
702
|
+
return {
|
|
703
|
+
"valid": False,
|
|
704
|
+
"error": f"Validation failed with exception: {str(e)}",
|
|
705
|
+
"error_type": "validation_error",
|
|
706
|
+
}
|
|
707
|
+
|
|
640
708
|
async def close(self) -> None:
|
|
641
709
|
"""Close all cached adapter connections.
|
|
642
710
|
|
|
@@ -17,13 +17,14 @@ Modules:
|
|
|
17
17
|
label_tools: Label management, normalization, deduplication, and cleanup
|
|
18
18
|
project_update_tools: Project status update management (1M-238)
|
|
19
19
|
project_status_tools: Project status analysis and work planning (1M-316)
|
|
20
|
+
milestone_tools: Milestone management and progress tracking (1M-607)
|
|
21
|
+
attachment_tools: File attachment management (ticket_attach, ticket_attachments)
|
|
20
22
|
|
|
21
23
|
Note:
|
|
22
24
|
instruction_tools: Removed from MCP server (CLI-only as of Phase 2 Sprint 2.3)
|
|
23
25
|
pr_tools: Removed from MCP server (CLI-only as of Phase 2 Sprint 1.3)
|
|
24
|
-
attachment_tools: Removed from MCP server (CLI-only as of Phase 2 Sprint 1.3)
|
|
25
26
|
These tools are available via CLI commands but not exposed through MCP interface.
|
|
26
|
-
Use
|
|
27
|
+
Use GitHub MCP for PR management.
|
|
27
28
|
|
|
28
29
|
"""
|
|
29
30
|
|
|
@@ -31,13 +32,14 @@ Note:
|
|
|
31
32
|
# Order matters - import core functionality first
|
|
32
33
|
from . import (
|
|
33
34
|
analysis_tools, # noqa: F401
|
|
34
|
-
#
|
|
35
|
+
attachment_tools, # noqa: F401
|
|
35
36
|
bulk_tools, # noqa: F401
|
|
36
37
|
comment_tools, # noqa: F401
|
|
37
38
|
config_tools, # noqa: F401
|
|
38
39
|
hierarchy_tools, # noqa: F401
|
|
39
40
|
# instruction_tools removed - CLI-only (Phase 2 Sprint 2.3)
|
|
40
41
|
label_tools, # noqa: F401
|
|
42
|
+
milestone_tools, # noqa: F401
|
|
41
43
|
# pr_tools removed - CLI-only (Phase 2 Sprint 1.3 - use GitHub MCP)
|
|
42
44
|
project_status_tools, # noqa: F401
|
|
43
45
|
project_update_tools, # noqa: F401
|
|
@@ -49,13 +51,14 @@ from . import (
|
|
|
49
51
|
|
|
50
52
|
__all__ = [
|
|
51
53
|
"analysis_tools",
|
|
52
|
-
|
|
54
|
+
"attachment_tools",
|
|
53
55
|
"bulk_tools",
|
|
54
56
|
"comment_tools",
|
|
55
57
|
"config_tools",
|
|
56
58
|
"hierarchy_tools",
|
|
57
59
|
# "instruction_tools" removed - CLI-only (Phase 2 Sprint 2.3)
|
|
58
60
|
"label_tools",
|
|
61
|
+
"milestone_tools",
|
|
59
62
|
# "pr_tools" removed - CLI-only (Phase 2 Sprint 1.3)
|
|
60
63
|
"project_status_tools",
|
|
61
64
|
"project_update_tools",
|
|
@@ -10,9 +10,10 @@ from pathlib import Path
|
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
12
|
from ....core.models import Comment, TicketType
|
|
13
|
-
from ..server_sdk import get_adapter
|
|
13
|
+
from ..server_sdk import get_adapter, mcp
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
@mcp.tool()
|
|
16
17
|
async def ticket_attach(
|
|
17
18
|
ticket_id: str,
|
|
18
19
|
file_path: str,
|
|
@@ -144,6 +145,7 @@ async def ticket_attach(
|
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
|
|
148
|
+
@mcp.tool()
|
|
147
149
|
async def ticket_attachments(
|
|
148
150
|
ticket_id: str,
|
|
149
151
|
) -> dict[str, Any]: # Keep as dict for MCP compatibility
|
|
@@ -26,6 +26,7 @@ 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
|
|
29
30
|
import warnings
|
|
30
31
|
from pathlib import Path
|
|
31
32
|
from typing import Any
|
|
@@ -39,6 +40,8 @@ from ....core.project_config import (
|
|
|
39
40
|
from ....core.registry import AdapterRegistry
|
|
40
41
|
from ..server_sdk import mcp
|
|
41
42
|
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
42
45
|
|
|
43
46
|
def get_resolver() -> ConfigResolver:
|
|
44
47
|
"""Get or create the configuration resolver.
|
|
@@ -57,6 +60,55 @@ def get_resolver() -> ConfigResolver:
|
|
|
57
60
|
return ConfigResolver(project_path=Path.cwd())
|
|
58
61
|
|
|
59
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
|
+
|
|
60
112
|
@mcp.tool()
|
|
61
113
|
async def config(
|
|
62
114
|
action: str,
|
|
@@ -68,7 +120,9 @@ async def config(
|
|
|
68
120
|
credentials: dict[str, Any] | None = None,
|
|
69
121
|
set_as_default: bool = True,
|
|
70
122
|
test_connection: bool = True,
|
|
71
|
-
**kwargs
|
|
123
|
+
# Explicitly define optional parameters (previously in **kwargs)
|
|
124
|
+
project_key: str | None = None,
|
|
125
|
+
user_email: str | None = None,
|
|
72
126
|
) -> dict[str, Any]:
|
|
73
127
|
"""Unified configuration management tool with action-based routing (v2.0.0).
|
|
74
128
|
|
|
@@ -79,6 +133,7 @@ async def config(
|
|
|
79
133
|
action: Operation to perform. Valid values:
|
|
80
134
|
- "get": Get current configuration
|
|
81
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)
|
|
82
137
|
- "validate": Validate all adapter configurations
|
|
83
138
|
- "test": Test adapter connectivity (requires adapter_name)
|
|
84
139
|
- "list_adapters": List all available adapters
|
|
@@ -93,7 +148,8 @@ async def config(
|
|
|
93
148
|
credentials: Adapter credentials dict (for action="setup_wizard")
|
|
94
149
|
set_as_default: Set adapter as default (for action="setup_wizard", default: True)
|
|
95
150
|
test_connection: Test connection during setup (for action="setup_wizard", default: True)
|
|
96
|
-
|
|
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")
|
|
97
153
|
|
|
98
154
|
Returns:
|
|
99
155
|
Response dict with status and action-specific data
|
|
@@ -150,6 +206,16 @@ async def config(
|
|
|
150
206
|
# Route based on action
|
|
151
207
|
if action_lower == "get":
|
|
152
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
|
+
)
|
|
153
219
|
elif action_lower == "set":
|
|
154
220
|
if key is None:
|
|
155
221
|
return {
|
|
@@ -163,7 +229,15 @@ async def config(
|
|
|
163
229
|
"error": "Parameter 'value' is required for action='set'",
|
|
164
230
|
"hint": "Use config(action='set', key='adapter', value='linear')",
|
|
165
231
|
}
|
|
166
|
-
|
|
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)
|
|
167
241
|
elif action_lower == "validate":
|
|
168
242
|
return await config_validate()
|
|
169
243
|
elif action_lower == "test":
|
|
@@ -207,6 +281,7 @@ async def config(
|
|
|
207
281
|
valid_actions = [
|
|
208
282
|
"get",
|
|
209
283
|
"set",
|
|
284
|
+
"set_project_from_url",
|
|
210
285
|
"validate",
|
|
211
286
|
"test",
|
|
212
287
|
"list_adapters",
|
|
@@ -359,9 +434,8 @@ async def config_set_primary_adapter(adapter: str) -> dict[str, Any]:
|
|
|
359
434
|
"valid_adapters": valid_adapters,
|
|
360
435
|
}
|
|
361
436
|
|
|
362
|
-
# Load current configuration
|
|
363
|
-
|
|
364
|
-
config = resolver.load_project_config() or TicketerConfig()
|
|
437
|
+
# Load current configuration safely (preserves adapters)
|
|
438
|
+
config = _safe_load_config()
|
|
365
439
|
|
|
366
440
|
# Store previous adapter for response
|
|
367
441
|
previous_adapter = config.default_adapter
|
|
@@ -370,6 +444,7 @@ async def config_set_primary_adapter(adapter: str) -> dict[str, Any]:
|
|
|
370
444
|
config.default_adapter = adapter.lower()
|
|
371
445
|
|
|
372
446
|
# Save configuration
|
|
447
|
+
resolver = get_resolver()
|
|
373
448
|
resolver.save_project_config(config)
|
|
374
449
|
|
|
375
450
|
return {
|
|
@@ -407,9 +482,8 @@ async def config_set_default_project(
|
|
|
407
482
|
stacklevel=2,
|
|
408
483
|
)
|
|
409
484
|
try:
|
|
410
|
-
# Load current configuration
|
|
411
|
-
|
|
412
|
-
config = resolver.load_project_config() or TicketerConfig()
|
|
485
|
+
# Load current configuration safely (preserves adapters)
|
|
486
|
+
config = _safe_load_config()
|
|
413
487
|
|
|
414
488
|
# Store previous project for response
|
|
415
489
|
previous_project = config.default_project or config.default_epic
|
|
@@ -419,6 +493,7 @@ async def config_set_default_project(
|
|
|
419
493
|
config.default_epic = project_id if project_id else None
|
|
420
494
|
|
|
421
495
|
# Save configuration
|
|
496
|
+
resolver = get_resolver()
|
|
422
497
|
resolver.save_project_config(config)
|
|
423
498
|
|
|
424
499
|
return {
|
|
@@ -459,9 +534,8 @@ async def config_set_default_user(
|
|
|
459
534
|
stacklevel=2,
|
|
460
535
|
)
|
|
461
536
|
try:
|
|
462
|
-
# Load current configuration
|
|
463
|
-
|
|
464
|
-
config = resolver.load_project_config() or TicketerConfig()
|
|
537
|
+
# Load current configuration safely (preserves adapters)
|
|
538
|
+
config = _safe_load_config()
|
|
465
539
|
|
|
466
540
|
# Store previous user for response
|
|
467
541
|
previous_user = config.default_user
|
|
@@ -470,6 +544,7 @@ async def config_set_default_user(
|
|
|
470
544
|
config.default_user = user_id if user_id else None
|
|
471
545
|
|
|
472
546
|
# Save configuration
|
|
547
|
+
resolver = get_resolver()
|
|
473
548
|
resolver.save_project_config(config)
|
|
474
549
|
|
|
475
550
|
return {
|
|
@@ -576,12 +651,14 @@ async def config_set_default_tags(
|
|
|
576
651
|
"error": f"Tag '{tag}' is too long (max 50 characters)",
|
|
577
652
|
}
|
|
578
653
|
|
|
579
|
-
# Load current configuration
|
|
580
|
-
|
|
581
|
-
config = resolver.load_project_config() or TicketerConfig()
|
|
654
|
+
# Load current configuration safely (preserves adapters)
|
|
655
|
+
config = _safe_load_config()
|
|
582
656
|
|
|
583
657
|
# Update config
|
|
584
658
|
config.default_tags = [tag.strip() for tag in tags]
|
|
659
|
+
|
|
660
|
+
# Save configuration
|
|
661
|
+
resolver = get_resolver()
|
|
585
662
|
resolver.save_project_config(config)
|
|
586
663
|
|
|
587
664
|
return {
|
|
@@ -624,15 +701,17 @@ async def config_set_default_team(
|
|
|
624
701
|
"error": "Team ID must be at least 1 character",
|
|
625
702
|
}
|
|
626
703
|
|
|
627
|
-
# Load current configuration
|
|
628
|
-
|
|
629
|
-
config = resolver.load_project_config() or TicketerConfig()
|
|
704
|
+
# Load current configuration safely (preserves adapters)
|
|
705
|
+
config = _safe_load_config()
|
|
630
706
|
|
|
631
707
|
# Store previous team for response
|
|
632
708
|
previous_team = config.default_team
|
|
633
709
|
|
|
634
710
|
# Update default team
|
|
635
711
|
config.default_team = team_id.strip()
|
|
712
|
+
|
|
713
|
+
# Save configuration
|
|
714
|
+
resolver = get_resolver()
|
|
636
715
|
resolver.save_project_config(config)
|
|
637
716
|
|
|
638
717
|
return {
|
|
@@ -676,15 +755,17 @@ async def config_set_default_cycle(
|
|
|
676
755
|
"error": "Cycle ID must be at least 1 character",
|
|
677
756
|
}
|
|
678
757
|
|
|
679
|
-
# Load current configuration
|
|
680
|
-
|
|
681
|
-
config = resolver.load_project_config() or TicketerConfig()
|
|
758
|
+
# Load current configuration safely (preserves adapters)
|
|
759
|
+
config = _safe_load_config()
|
|
682
760
|
|
|
683
761
|
# Store previous cycle for response
|
|
684
762
|
previous_cycle = config.default_cycle
|
|
685
763
|
|
|
686
764
|
# Update default cycle
|
|
687
765
|
config.default_cycle = cycle_id.strip()
|
|
766
|
+
|
|
767
|
+
# Save configuration
|
|
768
|
+
resolver = get_resolver()
|
|
688
769
|
resolver.save_project_config(config)
|
|
689
770
|
|
|
690
771
|
return {
|
|
@@ -727,13 +808,15 @@ async def config_set_default_epic(
|
|
|
727
808
|
"error": "Epic/project ID must be at least 2 characters",
|
|
728
809
|
}
|
|
729
810
|
|
|
730
|
-
# Load current configuration
|
|
731
|
-
|
|
732
|
-
config = resolver.load_project_config() or TicketerConfig()
|
|
811
|
+
# Load current configuration safely (preserves adapters)
|
|
812
|
+
config = _safe_load_config()
|
|
733
813
|
|
|
734
814
|
# Update config (set both for compatibility)
|
|
735
815
|
config.default_epic = epic_id.strip()
|
|
736
816
|
config.default_project = epic_id.strip()
|
|
817
|
+
|
|
818
|
+
# Save configuration
|
|
819
|
+
resolver = get_resolver()
|
|
737
820
|
resolver.save_project_config(config)
|
|
738
821
|
|
|
739
822
|
return {
|
|
@@ -776,10 +859,13 @@ async def config_set_assignment_labels(labels: list[str]) -> dict[str, Any]:
|
|
|
776
859
|
"error": f"Invalid label '{label}': must be 2-50 characters",
|
|
777
860
|
}
|
|
778
861
|
|
|
779
|
-
|
|
780
|
-
config =
|
|
862
|
+
# Load current configuration safely (preserves adapters)
|
|
863
|
+
config = _safe_load_config()
|
|
781
864
|
|
|
782
865
|
config.assignment_labels = labels if labels else None
|
|
866
|
+
|
|
867
|
+
# Save configuration
|
|
868
|
+
resolver = get_resolver()
|
|
783
869
|
resolver.save_project_config(config)
|
|
784
870
|
|
|
785
871
|
config_path = Path.cwd() / ".mcp-ticketer" / "config.json"
|
|
@@ -1292,10 +1378,11 @@ async def config_setup_wizard(
|
|
|
1292
1378
|
test_error = None
|
|
1293
1379
|
|
|
1294
1380
|
if test_connection:
|
|
1295
|
-
# Save config temporarily for testing
|
|
1296
|
-
|
|
1297
|
-
config = resolver.load_project_config() or TicketerConfig()
|
|
1381
|
+
# Save config temporarily for testing (preserves adapters)
|
|
1382
|
+
config = _safe_load_config()
|
|
1298
1383
|
config.adapters[adapter_lower] = adapter_config
|
|
1384
|
+
|
|
1385
|
+
resolver = get_resolver()
|
|
1299
1386
|
resolver.save_project_config(config)
|
|
1300
1387
|
|
|
1301
1388
|
# Test the adapter with enhanced error handling (1M-431)
|
|
@@ -1361,18 +1448,20 @@ async def config_setup_wizard(
|
|
|
1361
1448
|
],
|
|
1362
1449
|
}
|
|
1363
1450
|
else:
|
|
1364
|
-
# Save config without testing
|
|
1365
|
-
|
|
1366
|
-
config = resolver.load_project_config() or TicketerConfig()
|
|
1451
|
+
# Save config without testing (preserves adapters)
|
|
1452
|
+
config = _safe_load_config()
|
|
1367
1453
|
config.adapters[adapter_lower] = adapter_config
|
|
1454
|
+
|
|
1455
|
+
resolver = get_resolver()
|
|
1368
1456
|
resolver.save_project_config(config)
|
|
1369
1457
|
|
|
1370
1458
|
# Step 8: Set as default if enabled
|
|
1371
1459
|
if set_as_default:
|
|
1372
|
-
# Update default adapter
|
|
1373
|
-
|
|
1374
|
-
config = resolver.load_project_config() or TicketerConfig()
|
|
1460
|
+
# Update default adapter (preserves adapters)
|
|
1461
|
+
config = _safe_load_config()
|
|
1375
1462
|
config.default_adapter = adapter_lower
|
|
1463
|
+
|
|
1464
|
+
resolver = get_resolver()
|
|
1376
1465
|
resolver.save_project_config(config)
|
|
1377
1466
|
|
|
1378
1467
|
# Step 9: Return success
|
|
@@ -1398,6 +1487,115 @@ async def config_setup_wizard(
|
|
|
1398
1487
|
}
|
|
1399
1488
|
|
|
1400
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
|
+
|
|
1401
1599
|
def _mask_sensitive_values(config: dict[str, Any]) -> dict[str, Any]:
|
|
1402
1600
|
"""Mask sensitive values in configuration dictionary.
|
|
1403
1601
|
|