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.

Files changed (111) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/adapters/__init__.py +2 -0
  4. mcp_ticketer/adapters/aitrackdown.py +394 -9
  5. mcp_ticketer/adapters/asana/__init__.py +15 -0
  6. mcp_ticketer/adapters/asana/adapter.py +1416 -0
  7. mcp_ticketer/adapters/asana/client.py +292 -0
  8. mcp_ticketer/adapters/asana/mappers.py +348 -0
  9. mcp_ticketer/adapters/asana/types.py +146 -0
  10. mcp_ticketer/adapters/github.py +836 -105
  11. mcp_ticketer/adapters/hybrid.py +47 -5
  12. mcp_ticketer/adapters/jira.py +772 -1
  13. mcp_ticketer/adapters/linear/adapter.py +2293 -108
  14. mcp_ticketer/adapters/linear/client.py +146 -12
  15. mcp_ticketer/adapters/linear/mappers.py +105 -11
  16. mcp_ticketer/adapters/linear/queries.py +168 -1
  17. mcp_ticketer/adapters/linear/types.py +80 -4
  18. mcp_ticketer/analysis/__init__.py +56 -0
  19. mcp_ticketer/analysis/dependency_graph.py +255 -0
  20. mcp_ticketer/analysis/health_assessment.py +304 -0
  21. mcp_ticketer/analysis/orphaned.py +218 -0
  22. mcp_ticketer/analysis/project_status.py +594 -0
  23. mcp_ticketer/analysis/similarity.py +224 -0
  24. mcp_ticketer/analysis/staleness.py +266 -0
  25. mcp_ticketer/automation/__init__.py +11 -0
  26. mcp_ticketer/automation/project_updates.py +378 -0
  27. mcp_ticketer/cache/memory.py +3 -3
  28. mcp_ticketer/cli/adapter_diagnostics.py +4 -2
  29. mcp_ticketer/cli/auggie_configure.py +18 -6
  30. mcp_ticketer/cli/codex_configure.py +175 -60
  31. mcp_ticketer/cli/configure.py +884 -146
  32. mcp_ticketer/cli/cursor_configure.py +314 -0
  33. mcp_ticketer/cli/diagnostics.py +31 -28
  34. mcp_ticketer/cli/discover.py +293 -21
  35. mcp_ticketer/cli/gemini_configure.py +18 -6
  36. mcp_ticketer/cli/init_command.py +880 -0
  37. mcp_ticketer/cli/instruction_commands.py +435 -0
  38. mcp_ticketer/cli/linear_commands.py +99 -15
  39. mcp_ticketer/cli/main.py +109 -2055
  40. mcp_ticketer/cli/mcp_configure.py +673 -99
  41. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  42. mcp_ticketer/cli/migrate_config.py +12 -8
  43. mcp_ticketer/cli/platform_commands.py +6 -6
  44. mcp_ticketer/cli/platform_detection.py +477 -0
  45. mcp_ticketer/cli/platform_installer.py +536 -0
  46. mcp_ticketer/cli/project_update_commands.py +350 -0
  47. mcp_ticketer/cli/queue_commands.py +15 -15
  48. mcp_ticketer/cli/setup_command.py +639 -0
  49. mcp_ticketer/cli/simple_health.py +13 -11
  50. mcp_ticketer/cli/ticket_commands.py +277 -36
  51. mcp_ticketer/cli/update_checker.py +313 -0
  52. mcp_ticketer/cli/utils.py +45 -41
  53. mcp_ticketer/core/__init__.py +35 -1
  54. mcp_ticketer/core/adapter.py +170 -5
  55. mcp_ticketer/core/config.py +38 -31
  56. mcp_ticketer/core/env_discovery.py +33 -3
  57. mcp_ticketer/core/env_loader.py +7 -6
  58. mcp_ticketer/core/exceptions.py +10 -4
  59. mcp_ticketer/core/http_client.py +10 -10
  60. mcp_ticketer/core/instructions.py +405 -0
  61. mcp_ticketer/core/label_manager.py +732 -0
  62. mcp_ticketer/core/mappers.py +32 -20
  63. mcp_ticketer/core/models.py +136 -1
  64. mcp_ticketer/core/onepassword_secrets.py +379 -0
  65. mcp_ticketer/core/priority_matcher.py +463 -0
  66. mcp_ticketer/core/project_config.py +148 -14
  67. mcp_ticketer/core/registry.py +1 -1
  68. mcp_ticketer/core/session_state.py +171 -0
  69. mcp_ticketer/core/state_matcher.py +592 -0
  70. mcp_ticketer/core/url_parser.py +425 -0
  71. mcp_ticketer/core/validators.py +69 -0
  72. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  73. mcp_ticketer/mcp/__init__.py +2 -2
  74. mcp_ticketer/mcp/server/__init__.py +2 -2
  75. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  76. mcp_ticketer/mcp/server/main.py +187 -93
  77. mcp_ticketer/mcp/server/routing.py +655 -0
  78. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  79. mcp_ticketer/mcp/server/tools/__init__.py +37 -9
  80. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  81. mcp_ticketer/mcp/server/tools/attachment_tools.py +65 -20
  82. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  83. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  84. mcp_ticketer/mcp/server/tools/config_tools.py +1429 -0
  85. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  86. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +878 -319
  87. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  88. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  89. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  90. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  91. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  92. mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
  93. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  94. mcp_ticketer/mcp/server/tools/ticket_tools.py +1182 -82
  95. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  96. mcp_ticketer/queue/health_monitor.py +1 -0
  97. mcp_ticketer/queue/manager.py +4 -4
  98. mcp_ticketer/queue/queue.py +3 -3
  99. mcp_ticketer/queue/run_worker.py +1 -1
  100. mcp_ticketer/queue/ticket_registry.py +2 -2
  101. mcp_ticketer/queue/worker.py +15 -13
  102. mcp_ticketer/utils/__init__.py +5 -0
  103. mcp_ticketer/utils/token_utils.py +246 -0
  104. mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
  105. mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
  106. mcp_ticketer-0.4.11.dist-info/METADATA +0 -496
  107. mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
  108. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
  109. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
  110. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
  111. {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")
@@ -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
- epic = Epic(
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
- target_date=request.target_date,
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 = {"issue": issue.model_dump(), "tasks": []}
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 = await self.adapter.link_existing_pull_request(
738
- ticket_id=ticket_id,
739
- pr_url=pr_url,
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
- result = await self.adapter.link_to_pull_request(
755
- ticket_id=ticket_id,
756
- pr_url=pr_url,
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 result
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
- """Main entry point for MCP server - kept for backward compatibility.
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 .env files (highest priority for MCP)
1075
- env_config = _load_env_configuration()
1076
- if env_config and env_config.get("adapter_type"):
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
- try:
1101
- with open(config_file) as f:
1102
- config = json.load(f)
1103
- adapter_type = config.get("default_adapter", "aitrackdown")
1104
- # Get adapter-specific config
1105
- adapters_config = config.get("adapters", {})
1106
- adapter_config = adapters_config.get(adapter_type, {})
1107
- # Fallback to legacy config format
1108
- if not adapter_config and "config" in config:
1109
- adapter_config = config["config"]
1110
- logger.info(
1111
- f"Loaded MCP configuration from project-local: {config_file}"
1112
- )
1113
- except (OSError, json.JSONDecodeError) as e:
1114
- logger.warning(f"Could not load project config: {e}, using defaults")
1115
- adapter_type = "aitrackdown"
1116
- adapter_config = {"base_path": DEFAULT_BASE_PATH}
1117
- else:
1118
- # Priority 3: Default to aitrackdown
1119
- logger.info("No configuration found, defaulting to aitrackdown adapter")
1120
- adapter_type = "aitrackdown"
1121
- adapter_config = {"base_path": DEFAULT_BASE_PATH}
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
- Checks .env.local first (highest priority), then .env.
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
- from pathlib import Path
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
- if value: # Only add non-empty values
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