mcp-ticketer 0.4.11__py3-none-any.whl → 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +394 -9
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1416 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +348 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +836 -105
- mcp_ticketer/adapters/hybrid.py +47 -5
- mcp_ticketer/adapters/jira.py +772 -1
- mcp_ticketer/adapters/linear/adapter.py +2293 -108
- mcp_ticketer/adapters/linear/client.py +146 -12
- mcp_ticketer/adapters/linear/mappers.py +105 -11
- mcp_ticketer/adapters/linear/queries.py +168 -1
- mcp_ticketer/adapters/linear/types.py +80 -4
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -0
- mcp_ticketer/cache/memory.py +3 -3
- mcp_ticketer/cli/adapter_diagnostics.py +4 -2
- mcp_ticketer/cli/auggie_configure.py +18 -6
- mcp_ticketer/cli/codex_configure.py +175 -60
- mcp_ticketer/cli/configure.py +884 -146
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +31 -28
- mcp_ticketer/cli/discover.py +293 -21
- mcp_ticketer/cli/gemini_configure.py +18 -6
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +99 -15
- mcp_ticketer/cli/main.py +109 -2055
- mcp_ticketer/cli/mcp_configure.py +673 -99
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +6 -6
- mcp_ticketer/cli/platform_detection.py +477 -0
- mcp_ticketer/cli/platform_installer.py +536 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +13 -11
- mcp_ticketer/cli/ticket_commands.py +277 -36
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +45 -41
- mcp_ticketer/core/__init__.py +35 -1
- mcp_ticketer/core/adapter.py +170 -5
- mcp_ticketer/core/config.py +38 -31
- mcp_ticketer/core/env_discovery.py +33 -3
- mcp_ticketer/core/env_loader.py +7 -6
- mcp_ticketer/core/exceptions.py +10 -4
- mcp_ticketer/core/http_client.py +10 -10
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +32 -20
- mcp_ticketer/core/models.py +136 -1
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +148 -14
- mcp_ticketer/core/registry.py +1 -1
- mcp_ticketer/core/session_state.py +171 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +2 -2
- mcp_ticketer/mcp/server/__init__.py +2 -2
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/main.py +187 -93
- mcp_ticketer/mcp/server/routing.py +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +58 -0
- mcp_ticketer/mcp/server/tools/__init__.py +37 -9
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +65 -20
- mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
- mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
- mcp_ticketer/mcp/server/tools/config_tools.py +1429 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +878 -319
- mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1182 -82
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
- mcp_ticketer/queue/health_monitor.py +1 -0
- mcp_ticketer/queue/manager.py +4 -4
- mcp_ticketer/queue/queue.py +3 -3
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +2 -2
- mcp_ticketer/queue/worker.py +15 -13
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
- mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
- mcp_ticketer-0.4.11.dist-info/METADATA +0 -496
- mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Diagnostic helper for MCP error handling.
|
|
2
|
+
|
|
3
|
+
Provides quick diagnostic checks and error classification to help users
|
|
4
|
+
troubleshoot system configuration issues when MCP tools encounter errors.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from ...core.exceptions import (
|
|
12
|
+
AuthenticationError,
|
|
13
|
+
ConfigurationError,
|
|
14
|
+
NetworkError,
|
|
15
|
+
NotFoundError,
|
|
16
|
+
PermissionError,
|
|
17
|
+
RateLimitError,
|
|
18
|
+
StateTransitionError,
|
|
19
|
+
TimeoutError,
|
|
20
|
+
ValidationError,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ErrorSeverity(Enum):
|
|
27
|
+
"""Classification of error severity for diagnostic suggestions."""
|
|
28
|
+
|
|
29
|
+
CRITICAL = "critical" # Always suggest diagnostics
|
|
30
|
+
MEDIUM = "medium" # Suggest if pattern detected
|
|
31
|
+
LOW = "low" # Never suggest diagnostics
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Map exception types to severity levels
|
|
35
|
+
ERROR_SEVERITY_MAP = {
|
|
36
|
+
AuthenticationError: ErrorSeverity.CRITICAL,
|
|
37
|
+
ConfigurationError: ErrorSeverity.CRITICAL,
|
|
38
|
+
NetworkError: ErrorSeverity.CRITICAL,
|
|
39
|
+
TimeoutError: ErrorSeverity.CRITICAL,
|
|
40
|
+
NotFoundError: ErrorSeverity.MEDIUM,
|
|
41
|
+
PermissionError: ErrorSeverity.MEDIUM,
|
|
42
|
+
RateLimitError: ErrorSeverity.MEDIUM,
|
|
43
|
+
ValidationError: ErrorSeverity.LOW,
|
|
44
|
+
StateTransitionError: ErrorSeverity.LOW,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def should_suggest_diagnostics(exception: Exception) -> bool:
|
|
49
|
+
"""Determine if error response should include diagnostic suggestion.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
exception: The exception that was raised
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
True if diagnostics should be suggested
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
severity = get_error_severity(exception)
|
|
59
|
+
return severity in (ErrorSeverity.CRITICAL, ErrorSeverity.MEDIUM)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_error_severity(exception: Exception) -> ErrorSeverity:
|
|
63
|
+
"""Get severity level for an exception.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
exception: The exception to classify
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Error severity level
|
|
70
|
+
|
|
71
|
+
"""
|
|
72
|
+
exception_type = type(exception)
|
|
73
|
+
return ERROR_SEVERITY_MAP.get(exception_type, ErrorSeverity.MEDIUM)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def get_quick_diagnostic_info() -> dict[str, Any]:
|
|
77
|
+
"""Get lightweight diagnostic info without running full test suite.
|
|
78
|
+
|
|
79
|
+
Performs fast checks (< 100ms) to provide immediate troubleshooting hints:
|
|
80
|
+
- Adapter configuration status
|
|
81
|
+
- Credential presence
|
|
82
|
+
- Queue system status
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Dictionary with quick diagnostic results
|
|
86
|
+
|
|
87
|
+
"""
|
|
88
|
+
info: dict[str, Any] = {}
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
# Check adapter configuration (fast file read)
|
|
92
|
+
from ...cli.utils import CommonPatterns
|
|
93
|
+
|
|
94
|
+
config = CommonPatterns.load_config()
|
|
95
|
+
adapters = config.get("adapters", {})
|
|
96
|
+
|
|
97
|
+
info["adapter_configured"] = len(adapters) > 0
|
|
98
|
+
info["configured_adapters"] = list(adapters.keys())
|
|
99
|
+
info["default_adapter"] = config.get("default_adapter")
|
|
100
|
+
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.debug(f"Quick diagnostic config check failed: {e}")
|
|
103
|
+
info["adapter_configured"] = False
|
|
104
|
+
info["config_error"] = str(e)
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
# Check queue system status (fast status check, no operations)
|
|
108
|
+
from ...queue.worker import Worker
|
|
109
|
+
|
|
110
|
+
worker = Worker()
|
|
111
|
+
info["queue_running"] = worker.running
|
|
112
|
+
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.debug(f"Quick diagnostic queue check failed: {e}")
|
|
115
|
+
info["queue_running"] = False
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
# Get version information
|
|
119
|
+
from ...__version__ import __version__
|
|
120
|
+
|
|
121
|
+
info["mcp_ticketer_version"] = __version__
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.debug(f"Quick diagnostic version check failed: {e}")
|
|
125
|
+
info["mcp_ticketer_version"] = "unknown"
|
|
126
|
+
|
|
127
|
+
return info
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def build_diagnostic_suggestion(
|
|
131
|
+
exception: Exception, quick_info: dict[str, Any] | None = None
|
|
132
|
+
) -> dict[str, Any]:
|
|
133
|
+
"""Build diagnostic suggestion dict for error response.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
exception: The exception that occurred
|
|
137
|
+
quick_info: Optional quick diagnostic info (from get_quick_diagnostic_info())
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Diagnostic suggestion dictionary for inclusion in error response
|
|
141
|
+
|
|
142
|
+
"""
|
|
143
|
+
severity = get_error_severity(exception)
|
|
144
|
+
|
|
145
|
+
suggestion: dict[str, Any] = {
|
|
146
|
+
"severity": severity.value,
|
|
147
|
+
"message": _get_severity_message(severity),
|
|
148
|
+
"recommendation": _get_recommendation(severity),
|
|
149
|
+
"command": "Use the 'system_diagnostics' MCP tool or CLI: mcp-ticketer doctor",
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if quick_info:
|
|
153
|
+
suggestion["quick_checks"] = quick_info
|
|
154
|
+
|
|
155
|
+
return suggestion
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _get_severity_message(severity: ErrorSeverity) -> str:
|
|
159
|
+
"""Get human-readable message for severity level."""
|
|
160
|
+
messages = {
|
|
161
|
+
ErrorSeverity.CRITICAL: "This appears to be a system configuration issue",
|
|
162
|
+
ErrorSeverity.MEDIUM: "This may indicate a configuration or permission issue",
|
|
163
|
+
ErrorSeverity.LOW: "This is a validation or input error",
|
|
164
|
+
}
|
|
165
|
+
return messages.get(severity, "An error occurred")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _get_recommendation(severity: ErrorSeverity) -> str:
|
|
169
|
+
"""Get recommendation text for severity level."""
|
|
170
|
+
recommendations = {
|
|
171
|
+
ErrorSeverity.CRITICAL: "Run system diagnostics to identify the problem",
|
|
172
|
+
ErrorSeverity.MEDIUM: "Consider running diagnostics if the issue persists",
|
|
173
|
+
ErrorSeverity.LOW: "Check your input and try again",
|
|
174
|
+
}
|
|
175
|
+
return recommendations.get(severity, "Review the error message")
|
mcp_ticketer/mcp/server/main.py
CHANGED
|
@@ -48,23 +48,6 @@ from .dto import (
|
|
|
48
48
|
)
|
|
49
49
|
from .response_builder import ResponseBuilder
|
|
50
50
|
|
|
51
|
-
# Load environment variables early (prioritize .env.local)
|
|
52
|
-
# Check for .env.local first (takes precedence)
|
|
53
|
-
env_local_file = Path.cwd() / ".env.local"
|
|
54
|
-
if env_local_file.exists():
|
|
55
|
-
load_dotenv(env_local_file, override=True)
|
|
56
|
-
sys.stderr.write(f"[MCP Server] Loaded environment from: {env_local_file}\n")
|
|
57
|
-
else:
|
|
58
|
-
# Fall back to .env
|
|
59
|
-
env_file = Path.cwd() / ".env"
|
|
60
|
-
if env_file.exists():
|
|
61
|
-
load_dotenv(env_file, override=True)
|
|
62
|
-
sys.stderr.write(f"[MCP Server] Loaded environment from: {env_file}\n")
|
|
63
|
-
else:
|
|
64
|
-
# Try default dotenv loading (searches upward)
|
|
65
|
-
load_dotenv(override=True)
|
|
66
|
-
sys.stderr.write("[MCP Server] Loaded environment from default search path\n")
|
|
67
|
-
|
|
68
51
|
|
|
69
52
|
class MCPTicketServer:
|
|
70
53
|
"""MCP server for ticket operations over stdio - synchronous implementation."""
|
|
@@ -75,6 +58,7 @@ class MCPTicketServer:
|
|
|
75
58
|
"""Initialize MCP server.
|
|
76
59
|
|
|
77
60
|
Args:
|
|
61
|
+
----
|
|
78
62
|
adapter_type: Type of adapter to use
|
|
79
63
|
config: Adapter configuration
|
|
80
64
|
|
|
@@ -88,9 +72,11 @@ class MCPTicketServer:
|
|
|
88
72
|
"""Handle JSON-RPC request.
|
|
89
73
|
|
|
90
74
|
Args:
|
|
75
|
+
----
|
|
91
76
|
request: JSON-RPC request
|
|
92
77
|
|
|
93
78
|
Returns:
|
|
79
|
+
-------
|
|
94
80
|
JSON-RPC response
|
|
95
81
|
|
|
96
82
|
"""
|
|
@@ -175,11 +161,13 @@ class MCPTicketServer:
|
|
|
175
161
|
"""Create error response.
|
|
176
162
|
|
|
177
163
|
Args:
|
|
164
|
+
----
|
|
178
165
|
request_id: Request ID
|
|
179
166
|
code: Error code
|
|
180
167
|
message: Error message
|
|
181
168
|
|
|
182
169
|
Returns:
|
|
170
|
+
-------
|
|
183
171
|
Error response
|
|
184
172
|
|
|
185
173
|
"""
|
|
@@ -195,7 +183,7 @@ class MCPTicketServer:
|
|
|
195
183
|
request = CreateTicketRequest(**params)
|
|
196
184
|
|
|
197
185
|
# Build task from validated DTO
|
|
198
|
-
task = Task(
|
|
186
|
+
task = Task( # type: ignore[call-arg]
|
|
199
187
|
title=request.title,
|
|
200
188
|
description=request.description,
|
|
201
189
|
priority=Priority(request.priority),
|
|
@@ -273,7 +261,7 @@ class MCPTicketServer:
|
|
|
273
261
|
|
|
274
262
|
async def _handle_search(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
275
263
|
"""Handle ticket search - SYNCHRONOUS."""
|
|
276
|
-
query = SearchQuery(
|
|
264
|
+
query = SearchQuery( # type: ignore[call-arg]
|
|
277
265
|
query=params.get("query"),
|
|
278
266
|
state=TicketState(params["state"]) if params.get("state") else None,
|
|
279
267
|
priority=Priority(params["priority"]) if params.get("priority") else None,
|
|
@@ -309,7 +297,7 @@ class MCPTicketServer:
|
|
|
309
297
|
operation = params.get("operation", "add")
|
|
310
298
|
|
|
311
299
|
if operation == "add":
|
|
312
|
-
comment = Comment(
|
|
300
|
+
comment = Comment( # type: ignore[call-arg]
|
|
313
301
|
ticket_id=params["ticket_id"],
|
|
314
302
|
content=params["content"],
|
|
315
303
|
author=params.get("author"),
|
|
@@ -343,12 +331,17 @@ class MCPTicketServer:
|
|
|
343
331
|
request = CreateEpicRequest(**params)
|
|
344
332
|
|
|
345
333
|
# Build epic from validated DTO
|
|
346
|
-
|
|
334
|
+
metadata: dict[str, Any] = {}
|
|
335
|
+
if request.target_date:
|
|
336
|
+
metadata["target_date"] = request.target_date
|
|
337
|
+
if request.lead_id:
|
|
338
|
+
metadata["lead_id"] = request.lead_id
|
|
339
|
+
|
|
340
|
+
epic = Epic( # type: ignore[call-arg]
|
|
347
341
|
title=request.title,
|
|
348
342
|
description=request.description,
|
|
349
343
|
child_issues=request.child_issues,
|
|
350
|
-
|
|
351
|
-
lead_id=request.lead_id,
|
|
344
|
+
metadata=metadata,
|
|
352
345
|
)
|
|
353
346
|
|
|
354
347
|
# Create directly
|
|
@@ -389,7 +382,7 @@ class MCPTicketServer:
|
|
|
389
382
|
request = CreateIssueRequest(**params)
|
|
390
383
|
|
|
391
384
|
# Build task (issue) from validated DTO
|
|
392
|
-
task = Task(
|
|
385
|
+
task = Task( # type: ignore[call-arg]
|
|
393
386
|
title=request.title,
|
|
394
387
|
description=request.description,
|
|
395
388
|
parent_epic=request.epic_id, # Issues are tasks under epics
|
|
@@ -422,7 +415,7 @@ class MCPTicketServer:
|
|
|
422
415
|
request = CreateTaskRequest(**params)
|
|
423
416
|
|
|
424
417
|
# Build task from validated DTO
|
|
425
|
-
task = Task(
|
|
418
|
+
task = Task( # type: ignore[call-arg]
|
|
426
419
|
title=request.title,
|
|
427
420
|
parent_issue=request.parent_id,
|
|
428
421
|
description=request.description,
|
|
@@ -454,16 +447,19 @@ class MCPTicketServer:
|
|
|
454
447
|
)
|
|
455
448
|
|
|
456
449
|
# Build tree structure
|
|
457
|
-
tree = {"epic": epic.model_dump(), "issues": []}
|
|
450
|
+
tree: dict[str, Any] = {"epic": epic.model_dump(), "issues": []}
|
|
458
451
|
|
|
459
452
|
# Get issues in epic if depth allows (depth 1 = epic only, depth 2+ = issues)
|
|
460
453
|
if max_depth > 1:
|
|
461
454
|
issues = await self.adapter.list_issues_by_epic(epic_id)
|
|
462
455
|
for issue in issues:
|
|
463
|
-
issue_node
|
|
456
|
+
issue_node: dict[str, Any] = {
|
|
457
|
+
"issue": issue.model_dump(),
|
|
458
|
+
"tasks": [],
|
|
459
|
+
}
|
|
464
460
|
|
|
465
461
|
# Get tasks in issue if depth allows (depth 3+ = tasks)
|
|
466
|
-
if max_depth > 2:
|
|
462
|
+
if max_depth > 2 and issue.id:
|
|
467
463
|
tasks = await self.adapter.list_tasks_by_issue(issue.id)
|
|
468
464
|
issue_node["tasks"] = [task.model_dump() for task in tasks]
|
|
469
465
|
|
|
@@ -561,7 +557,7 @@ class MCPTicketServer:
|
|
|
561
557
|
include_parents = params.get("include_parents", True)
|
|
562
558
|
|
|
563
559
|
# Perform basic search
|
|
564
|
-
search_query = SearchQuery(
|
|
560
|
+
search_query = SearchQuery( # type: ignore[call-arg]
|
|
565
561
|
query=query,
|
|
566
562
|
state=TicketState(params["state"]) if params.get("state") else None,
|
|
567
563
|
priority=Priority(params["priority"]) if params.get("priority") else None,
|
|
@@ -666,6 +662,12 @@ class MCPTicketServer:
|
|
|
666
662
|
"error": str(e),
|
|
667
663
|
"ticket_id": ticket_id,
|
|
668
664
|
}
|
|
665
|
+
# Fallback if not GitHub adapter instance
|
|
666
|
+
return {
|
|
667
|
+
"success": False,
|
|
668
|
+
"error": "GitHub adapter not properly initialized",
|
|
669
|
+
"ticket_id": ticket_id,
|
|
670
|
+
}
|
|
669
671
|
elif "linear" in adapter_name:
|
|
670
672
|
# Linear adapter needs GitHub config for PR creation
|
|
671
673
|
from ..adapters.linear import LinearAdapter
|
|
@@ -710,6 +712,12 @@ class MCPTicketServer:
|
|
|
710
712
|
"error": str(e),
|
|
711
713
|
"ticket_id": ticket_id,
|
|
712
714
|
}
|
|
715
|
+
# Fallback if not Linear adapter instance
|
|
716
|
+
return {
|
|
717
|
+
"success": False,
|
|
718
|
+
"error": "Linear adapter not properly initialized",
|
|
719
|
+
"ticket_id": ticket_id,
|
|
720
|
+
}
|
|
713
721
|
else:
|
|
714
722
|
return {
|
|
715
723
|
"success": False,
|
|
@@ -734,9 +742,11 @@ class MCPTicketServer:
|
|
|
734
742
|
|
|
735
743
|
if isinstance(self.adapter, GitHubAdapter):
|
|
736
744
|
try:
|
|
737
|
-
result =
|
|
738
|
-
|
|
739
|
-
|
|
745
|
+
result: dict[str, Any] = (
|
|
746
|
+
await self.adapter.link_existing_pull_request(
|
|
747
|
+
ticket_id=ticket_id,
|
|
748
|
+
pr_url=pr_url,
|
|
749
|
+
)
|
|
740
750
|
)
|
|
741
751
|
return result
|
|
742
752
|
except Exception as e:
|
|
@@ -746,16 +756,25 @@ class MCPTicketServer:
|
|
|
746
756
|
"ticket_id": ticket_id,
|
|
747
757
|
"pr_url": pr_url,
|
|
748
758
|
}
|
|
759
|
+
# Fallback if not GitHub adapter instance
|
|
760
|
+
return {
|
|
761
|
+
"success": False,
|
|
762
|
+
"error": "GitHub adapter not properly initialized",
|
|
763
|
+
"ticket_id": ticket_id,
|
|
764
|
+
"pr_url": pr_url,
|
|
765
|
+
}
|
|
749
766
|
elif "linear" in adapter_name:
|
|
750
767
|
from ..adapters.linear import LinearAdapter
|
|
751
768
|
|
|
752
769
|
if isinstance(self.adapter, LinearAdapter):
|
|
753
770
|
try:
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
771
|
+
link_result: dict[str, Any] = (
|
|
772
|
+
await self.adapter.link_to_pull_request(
|
|
773
|
+
ticket_id=ticket_id,
|
|
774
|
+
pr_url=pr_url,
|
|
775
|
+
)
|
|
757
776
|
)
|
|
758
|
-
return
|
|
777
|
+
return link_result
|
|
759
778
|
except Exception as e:
|
|
760
779
|
return {
|
|
761
780
|
"success": False,
|
|
@@ -763,6 +782,13 @@ class MCPTicketServer:
|
|
|
763
782
|
"ticket_id": ticket_id,
|
|
764
783
|
"pr_url": pr_url,
|
|
765
784
|
}
|
|
785
|
+
# Fallback if not Linear adapter instance
|
|
786
|
+
return {
|
|
787
|
+
"success": False,
|
|
788
|
+
"error": "Linear adapter not properly initialized",
|
|
789
|
+
"ticket_id": ticket_id,
|
|
790
|
+
"pr_url": pr_url,
|
|
791
|
+
}
|
|
766
792
|
else:
|
|
767
793
|
return {
|
|
768
794
|
"success": False,
|
|
@@ -775,9 +801,11 @@ class MCPTicketServer:
|
|
|
775
801
|
"""Handle initialize request from MCP client.
|
|
776
802
|
|
|
777
803
|
Args:
|
|
804
|
+
----
|
|
778
805
|
params: Initialize parameters
|
|
779
806
|
|
|
780
807
|
Returns:
|
|
808
|
+
-------
|
|
781
809
|
Server capabilities
|
|
782
810
|
|
|
783
811
|
"""
|
|
@@ -906,9 +934,11 @@ class MCPTicketServer:
|
|
|
906
934
|
"""Handle tool invocation from MCP client.
|
|
907
935
|
|
|
908
936
|
Args:
|
|
937
|
+
----
|
|
909
938
|
params: Contains 'name' and 'arguments' fields
|
|
910
939
|
|
|
911
940
|
Returns:
|
|
941
|
+
-------
|
|
912
942
|
MCP formatted response with content array
|
|
913
943
|
|
|
914
944
|
"""
|
|
@@ -1050,8 +1080,8 @@ class MCPTicketServer:
|
|
|
1050
1080
|
await self.adapter.close()
|
|
1051
1081
|
|
|
1052
1082
|
|
|
1053
|
-
async def main():
|
|
1054
|
-
"""
|
|
1083
|
+
async def main() -> None:
|
|
1084
|
+
"""Run main entry point for MCP server - kept for backward compatibility.
|
|
1055
1085
|
|
|
1056
1086
|
This function is maintained in case it's being called directly,
|
|
1057
1087
|
but the preferred way is now through the CLI: `mcp-ticketer mcp`
|
|
@@ -1063,62 +1093,94 @@ async def main():
|
|
|
1063
1093
|
# Load configuration
|
|
1064
1094
|
import json
|
|
1065
1095
|
import logging
|
|
1066
|
-
from pathlib import Path
|
|
1067
1096
|
|
|
1068
1097
|
logger = logging.getLogger(__name__)
|
|
1069
1098
|
|
|
1099
|
+
# Load environment variables AFTER working directory has been set by __main__.py
|
|
1100
|
+
# This ensures we load .env files from the target project directory, not from where the command is executed
|
|
1101
|
+
env_local_file = Path.cwd() / ".env.local"
|
|
1102
|
+
if env_local_file.exists():
|
|
1103
|
+
load_dotenv(env_local_file, override=True)
|
|
1104
|
+
sys.stderr.write(f"[MCP Server] Loaded environment from: {env_local_file}\n")
|
|
1105
|
+
logger.debug(f"Loaded environment from: {env_local_file}")
|
|
1106
|
+
else:
|
|
1107
|
+
# Fall back to .env
|
|
1108
|
+
env_file = Path.cwd() / ".env"
|
|
1109
|
+
if env_file.exists():
|
|
1110
|
+
load_dotenv(env_file, override=True)
|
|
1111
|
+
sys.stderr.write(f"[MCP Server] Loaded environment from: {env_file}\n")
|
|
1112
|
+
logger.debug(f"Loaded environment from: {env_file}")
|
|
1113
|
+
else:
|
|
1114
|
+
# Try default dotenv loading (searches upward)
|
|
1115
|
+
load_dotenv(override=True)
|
|
1116
|
+
sys.stderr.write(
|
|
1117
|
+
"[MCP Server] Loaded environment from default search path\n"
|
|
1118
|
+
)
|
|
1119
|
+
logger.debug("Loaded environment from default search path")
|
|
1120
|
+
|
|
1070
1121
|
# Initialize defaults
|
|
1071
1122
|
adapter_type = "aitrackdown"
|
|
1072
1123
|
adapter_config = {"base_path": DEFAULT_BASE_PATH}
|
|
1073
1124
|
|
|
1074
|
-
# Priority 1: Check
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
adapter_type = env_config["adapter_type"]
|
|
1078
|
-
adapter_config = env_config["adapter_config"]
|
|
1079
|
-
logger.info(f"Using adapter from .env files: {adapter_type}")
|
|
1080
|
-
logger.info(f"Built adapter config from .env: {list(adapter_config.keys())}")
|
|
1081
|
-
else:
|
|
1082
|
-
# Priority 2: Check project-local config file
|
|
1083
|
-
config_file = Path.cwd() / ".mcp-ticketer" / "config.json"
|
|
1084
|
-
if config_file.exists():
|
|
1085
|
-
# Validate config is within project
|
|
1086
|
-
try:
|
|
1087
|
-
if not config_file.resolve().is_relative_to(Path.cwd().resolve()):
|
|
1088
|
-
logger.error(
|
|
1089
|
-
f"Security violation: Config file {config_file} "
|
|
1090
|
-
"is not within project directory"
|
|
1091
|
-
)
|
|
1092
|
-
raise ValueError(
|
|
1093
|
-
f"Security violation: Config file {config_file} "
|
|
1094
|
-
"is not within project directory"
|
|
1095
|
-
)
|
|
1096
|
-
except (ValueError, RuntimeError):
|
|
1097
|
-
# is_relative_to may raise ValueError in some cases
|
|
1098
|
-
pass
|
|
1125
|
+
# Priority 1: Check project-local config file (highest priority)
|
|
1126
|
+
config_file = Path.cwd() / ".mcp-ticketer" / "config.json"
|
|
1127
|
+
config_loaded = False
|
|
1099
1128
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1129
|
+
if config_file.exists():
|
|
1130
|
+
# Validate config is within project
|
|
1131
|
+
try:
|
|
1132
|
+
if not config_file.resolve().is_relative_to(Path.cwd().resolve()):
|
|
1133
|
+
logger.error(
|
|
1134
|
+
f"Security violation: Config file {config_file} "
|
|
1135
|
+
"is not within project directory"
|
|
1136
|
+
)
|
|
1137
|
+
raise ValueError(
|
|
1138
|
+
f"Security violation: Config file {config_file} "
|
|
1139
|
+
"is not within project directory"
|
|
1140
|
+
)
|
|
1141
|
+
except (ValueError, RuntimeError):
|
|
1142
|
+
# is_relative_to may raise ValueError in some cases
|
|
1143
|
+
pass
|
|
1144
|
+
|
|
1145
|
+
try:
|
|
1146
|
+
with open(config_file) as f:
|
|
1147
|
+
config = json.load(f)
|
|
1148
|
+
adapter_type = config.get("default_adapter", "aitrackdown")
|
|
1149
|
+
# Get adapter-specific config
|
|
1150
|
+
adapters_config = config.get("adapters", {})
|
|
1151
|
+
adapter_config = adapters_config.get(adapter_type, {})
|
|
1152
|
+
# Fallback to legacy config format
|
|
1153
|
+
if not adapter_config and "config" in config:
|
|
1154
|
+
adapter_config = config["config"]
|
|
1155
|
+
config_loaded = True
|
|
1156
|
+
logger.info(
|
|
1157
|
+
f"Loaded MCP configuration from project-local: {config_file}"
|
|
1158
|
+
)
|
|
1159
|
+
sys.stderr.write(
|
|
1160
|
+
f"[MCP Server] Using adapter from config: {adapter_type}\n"
|
|
1161
|
+
)
|
|
1162
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
1163
|
+
logger.warning(f"Could not load project config: {e}, will try .env files")
|
|
1164
|
+
|
|
1165
|
+
# Priority 2: Check .env files (only if no config file found)
|
|
1166
|
+
if not config_loaded:
|
|
1167
|
+
env_config = _load_env_configuration()
|
|
1168
|
+
if env_config and env_config.get("adapter_type"):
|
|
1169
|
+
adapter_type = env_config["adapter_type"]
|
|
1170
|
+
adapter_config = env_config["adapter_config"]
|
|
1171
|
+
config_loaded = True
|
|
1172
|
+
logger.info(f"Using adapter from .env files: {adapter_type}")
|
|
1173
|
+
logger.info(
|
|
1174
|
+
f"Built adapter config from .env: {list(adapter_config.keys())}"
|
|
1175
|
+
)
|
|
1176
|
+
sys.stderr.write(f"[MCP Server] Using adapter from .env: {adapter_type}\n")
|
|
1177
|
+
|
|
1178
|
+
# Priority 3: Default to aitrackdown
|
|
1179
|
+
if not config_loaded:
|
|
1180
|
+
logger.info("No configuration found, defaulting to aitrackdown adapter")
|
|
1181
|
+
sys.stderr.write("[MCP Server] No config found, using default: aitrackdown\n")
|
|
1182
|
+
adapter_type = "aitrackdown"
|
|
1183
|
+
adapter_config = {"base_path": DEFAULT_BASE_PATH}
|
|
1122
1184
|
|
|
1123
1185
|
# Log final configuration for debugging
|
|
1124
1186
|
logger.info(f"Starting MCP server with adapter: {adapter_type}")
|
|
@@ -1130,20 +1192,48 @@ async def main():
|
|
|
1130
1192
|
|
|
1131
1193
|
|
|
1132
1194
|
def _load_env_configuration() -> dict[str, Any] | None:
|
|
1133
|
-
"""Load adapter configuration from .env files.
|
|
1195
|
+
"""Load adapter configuration from environment variables and .env files.
|
|
1134
1196
|
|
|
1135
|
-
|
|
1197
|
+
Priority order (highest to lowest):
|
|
1198
|
+
1. os.environ (set by MCP clients like Claude Desktop)
|
|
1199
|
+
2. .env.local file (local overrides)
|
|
1200
|
+
3. .env file (default configuration)
|
|
1136
1201
|
|
|
1137
1202
|
Returns:
|
|
1203
|
+
-------
|
|
1138
1204
|
Dictionary with 'adapter_type' and 'adapter_config' keys, or None if no config found
|
|
1139
1205
|
|
|
1140
1206
|
"""
|
|
1141
|
-
|
|
1207
|
+
import os
|
|
1142
1208
|
|
|
1143
|
-
# Check for .env files in order of preference
|
|
1144
|
-
env_files = [".env.local", ".env"]
|
|
1145
1209
|
env_vars = {}
|
|
1146
1210
|
|
|
1211
|
+
# Priority 1: Check process environment variables (set by MCP client)
|
|
1212
|
+
# This allows Claude Desktop and other MCP clients to configure the adapter
|
|
1213
|
+
relevant_env_keys = [
|
|
1214
|
+
"MCP_TICKETER_ADAPTER",
|
|
1215
|
+
"LINEAR_API_KEY",
|
|
1216
|
+
"LINEAR_TEAM_ID",
|
|
1217
|
+
"LINEAR_TEAM_KEY",
|
|
1218
|
+
"LINEAR_API_URL",
|
|
1219
|
+
"JIRA_SERVER",
|
|
1220
|
+
"JIRA_EMAIL",
|
|
1221
|
+
"JIRA_API_TOKEN",
|
|
1222
|
+
"JIRA_PROJECT_KEY",
|
|
1223
|
+
"GITHUB_TOKEN",
|
|
1224
|
+
"GITHUB_OWNER",
|
|
1225
|
+
"GITHUB_REPO",
|
|
1226
|
+
"MCP_TICKETER_BASE_PATH",
|
|
1227
|
+
]
|
|
1228
|
+
|
|
1229
|
+
for key in relevant_env_keys:
|
|
1230
|
+
if os.environ.get(key):
|
|
1231
|
+
env_vars[key] = os.environ[key]
|
|
1232
|
+
|
|
1233
|
+
# Priority 2: Check .env files (only for keys not already set)
|
|
1234
|
+
# This allows .env files to provide fallback values
|
|
1235
|
+
env_files = [".env.local", ".env"]
|
|
1236
|
+
|
|
1147
1237
|
for env_file in env_files:
|
|
1148
1238
|
env_path = Path.cwd() / env_file
|
|
1149
1239
|
if env_path.exists():
|
|
@@ -1156,7 +1246,9 @@ def _load_env_configuration() -> dict[str, Any] | None:
|
|
|
1156
1246
|
key, value = line.split("=", 1)
|
|
1157
1247
|
key = key.strip()
|
|
1158
1248
|
value = value.strip().strip('"').strip("'")
|
|
1159
|
-
|
|
1249
|
+
|
|
1250
|
+
# Only set if not already in env_vars (os.environ takes priority)
|
|
1251
|
+
if key not in env_vars and value:
|
|
1160
1252
|
env_vars[key] = value
|
|
1161
1253
|
except Exception:
|
|
1162
1254
|
continue
|
|
@@ -1192,14 +1284,16 @@ def _build_adapter_config_from_env_vars(
|
|
|
1192
1284
|
"""Build adapter configuration from parsed environment variables.
|
|
1193
1285
|
|
|
1194
1286
|
Args:
|
|
1287
|
+
----
|
|
1195
1288
|
adapter_type: Type of adapter to configure
|
|
1196
1289
|
env_vars: Dictionary of environment variables from .env files
|
|
1197
1290
|
|
|
1198
1291
|
Returns:
|
|
1292
|
+
-------
|
|
1199
1293
|
Dictionary of adapter configuration
|
|
1200
1294
|
|
|
1201
1295
|
"""
|
|
1202
|
-
config = {}
|
|
1296
|
+
config: dict[str, Any] = {}
|
|
1203
1297
|
|
|
1204
1298
|
if adapter_type == "linear":
|
|
1205
1299
|
# Linear adapter configuration
|