mcp-ticketer 0.3.1__py3-none-any.whl → 0.3.3__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 (41) hide show
  1. mcp_ticketer/__version__.py +1 -1
  2. mcp_ticketer/adapters/aitrackdown.py +164 -36
  3. mcp_ticketer/adapters/github.py +11 -8
  4. mcp_ticketer/adapters/jira.py +29 -28
  5. mcp_ticketer/adapters/linear/__init__.py +1 -1
  6. mcp_ticketer/adapters/linear/adapter.py +105 -104
  7. mcp_ticketer/adapters/linear/client.py +78 -59
  8. mcp_ticketer/adapters/linear/mappers.py +93 -73
  9. mcp_ticketer/adapters/linear/queries.py +28 -7
  10. mcp_ticketer/adapters/linear/types.py +67 -60
  11. mcp_ticketer/adapters/linear.py +2 -2
  12. mcp_ticketer/cli/adapter_diagnostics.py +87 -52
  13. mcp_ticketer/cli/codex_configure.py +6 -6
  14. mcp_ticketer/cli/diagnostics.py +180 -88
  15. mcp_ticketer/cli/linear_commands.py +156 -113
  16. mcp_ticketer/cli/main.py +153 -82
  17. mcp_ticketer/cli/simple_health.py +74 -51
  18. mcp_ticketer/cli/utils.py +15 -10
  19. mcp_ticketer/core/config.py +23 -19
  20. mcp_ticketer/core/env_discovery.py +5 -4
  21. mcp_ticketer/core/env_loader.py +114 -91
  22. mcp_ticketer/core/exceptions.py +22 -20
  23. mcp_ticketer/core/models.py +9 -0
  24. mcp_ticketer/core/project_config.py +1 -1
  25. mcp_ticketer/mcp/constants.py +58 -0
  26. mcp_ticketer/mcp/dto.py +195 -0
  27. mcp_ticketer/mcp/response_builder.py +206 -0
  28. mcp_ticketer/mcp/server.py +361 -1182
  29. mcp_ticketer/queue/health_monitor.py +166 -135
  30. mcp_ticketer/queue/manager.py +70 -19
  31. mcp_ticketer/queue/queue.py +24 -5
  32. mcp_ticketer/queue/run_worker.py +1 -1
  33. mcp_ticketer/queue/ticket_registry.py +203 -145
  34. mcp_ticketer/queue/worker.py +79 -43
  35. {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.3.dist-info}/METADATA +1 -1
  36. mcp_ticketer-0.3.3.dist-info/RECORD +62 -0
  37. mcp_ticketer-0.3.1.dist-info/RECORD +0 -59
  38. {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.3.dist-info}/WHEEL +0 -0
  39. {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.3.dist-info}/entry_points.txt +0 -0
  40. {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.3.dist-info}/licenses/LICENSE +0 -0
  41. {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,58 @@
1
+ """MCP server constants and configuration."""
2
+
3
+ # JSON-RPC Protocol
4
+ JSONRPC_VERSION = "2.0"
5
+ MCP_PROTOCOL_VERSION = "2024-11-05"
6
+
7
+ # Server Info
8
+ SERVER_NAME = "mcp-ticketer"
9
+ SERVER_VERSION = "0.3.2"
10
+
11
+ # Status Values
12
+ STATUS_COMPLETED = "completed"
13
+ STATUS_ERROR = "error"
14
+ STATUS_NOT_IMPLEMENTED = "not_implemented"
15
+
16
+ # Error Codes
17
+ ERROR_PARSE = -32700
18
+ ERROR_INVALID_REQUEST = -32600
19
+ ERROR_METHOD_NOT_FOUND = -32601
20
+ ERROR_INVALID_PARAMS = -32602
21
+ ERROR_INTERNAL = -32603
22
+
23
+ # Default Values
24
+ DEFAULT_LIMIT = 10
25
+ DEFAULT_OFFSET = 0
26
+ DEFAULT_PRIORITY = "medium"
27
+ DEFAULT_MAX_DEPTH = 3
28
+ DEFAULT_BASE_PATH = ".aitrackdown"
29
+
30
+ # Response Messages
31
+ MSG_TICKET_NOT_FOUND = "Ticket {ticket_id} not found"
32
+ MSG_UPDATE_FAILED = "Ticket {ticket_id} not found or update failed"
33
+ MSG_TRANSITION_FAILED = "Ticket {ticket_id} not found or transition failed"
34
+ MSG_EPIC_NOT_FOUND = "Epic {epic_id} not found"
35
+ MSG_MISSING_PARENT_ID = "Tasks must have a parent_id (issue identifier)"
36
+ MSG_UNKNOWN_OPERATION = "Unknown comment operation: {operation}"
37
+ MSG_UNKNOWN_METHOD = "Method not found: {method}"
38
+ MSG_INTERNAL_ERROR = "Internal error: {error}"
39
+ MSG_NO_TICKETS_PROVIDED = "No tickets provided for bulk creation"
40
+ MSG_NO_UPDATES_PROVIDED = "No updates provided for bulk operation"
41
+ MSG_MISSING_TITLE = "Ticket {index} missing required 'title' field"
42
+ MSG_MISSING_TICKET_ID = "Update {index} missing required 'ticket_id' field"
43
+ MSG_TICKET_ID_REQUIRED = "ticket_id is required"
44
+ MSG_PR_URL_REQUIRED = "pr_url is required"
45
+ MSG_ATTACHMENT_NOT_IMPLEMENTED = "Attachment functionality not yet implemented"
46
+ MSG_PR_NOT_SUPPORTED = "PR creation not supported for adapter: {adapter}"
47
+ MSG_PR_LINK_NOT_SUPPORTED = "PR linking not supported for adapter: {adapter}"
48
+ MSG_UNKNOWN_TOOL = "Unknown tool: {tool}"
49
+ MSG_GITHUB_CONFIG_REQUIRED = "GitHub owner and repo are required for Linear PR creation"
50
+
51
+ # Attachment Alternative Messages
52
+ ATTACHMENT_ALTERNATIVES = [
53
+ "Add file URLs in comments",
54
+ "Use external file storage",
55
+ ]
56
+ ATTACHMENT_NOT_IMPLEMENTED_REASON = (
57
+ "File attachments require adapter-specific implementation"
58
+ )
@@ -0,0 +1,195 @@
1
+ """Data Transfer Objects for MCP requests and responses."""
2
+
3
+ from typing import Any, Optional
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ # Request DTOs
9
+ class CreateTicketRequest(BaseModel):
10
+ """Request to create a ticket."""
11
+
12
+ title: str = Field(..., min_length=1, description="Ticket title")
13
+ description: Optional[str] = Field(None, description="Ticket description")
14
+ priority: str = Field("medium", description="Ticket priority")
15
+ tags: list[str] = Field(default_factory=list, description="Ticket tags")
16
+ assignee: Optional[str] = Field(None, description="Ticket assignee")
17
+
18
+
19
+ class CreateEpicRequest(BaseModel):
20
+ """Request to create an epic."""
21
+
22
+ title: str = Field(..., min_length=1, description="Epic title")
23
+ description: Optional[str] = Field(None, description="Epic description")
24
+ child_issues: list[str] = Field(default_factory=list, description="Child issue IDs")
25
+ target_date: Optional[str] = Field(None, description="Target completion date")
26
+ lead_id: Optional[str] = Field(None, description="Epic lead/owner ID")
27
+
28
+
29
+ class CreateIssueRequest(BaseModel):
30
+ """Request to create an issue."""
31
+
32
+ title: str = Field(..., min_length=1, description="Issue title")
33
+ description: Optional[str] = Field(None, description="Issue description")
34
+ epic_id: Optional[str] = Field(None, description="Parent epic ID")
35
+ priority: str = Field("medium", description="Issue priority")
36
+ assignee: Optional[str] = Field(None, description="Issue assignee")
37
+ tags: list[str] = Field(default_factory=list, description="Issue tags")
38
+ estimated_hours: Optional[float] = Field(
39
+ None, description="Estimated hours to complete"
40
+ )
41
+
42
+
43
+ class CreateTaskRequest(BaseModel):
44
+ """Request to create a task."""
45
+
46
+ title: str = Field(..., min_length=1, description="Task title")
47
+ parent_id: str = Field(..., description="Parent issue ID")
48
+ description: Optional[str] = Field(None, description="Task description")
49
+ priority: str = Field("medium", description="Task priority")
50
+ assignee: Optional[str] = Field(None, description="Task assignee")
51
+ tags: list[str] = Field(default_factory=list, description="Task tags")
52
+ estimated_hours: Optional[float] = Field(
53
+ None, description="Estimated hours to complete"
54
+ )
55
+
56
+
57
+ class ReadTicketRequest(BaseModel):
58
+ """Request to read a ticket."""
59
+
60
+ ticket_id: str = Field(..., description="Ticket ID to read")
61
+
62
+
63
+ class UpdateTicketRequest(BaseModel):
64
+ """Request to update a ticket."""
65
+
66
+ ticket_id: str = Field(..., description="Ticket ID to update")
67
+ updates: dict[str, Any] = Field(..., description="Fields to update")
68
+
69
+
70
+ class TransitionRequest(BaseModel):
71
+ """Request to transition ticket state."""
72
+
73
+ ticket_id: str = Field(..., description="Ticket ID")
74
+ target_state: str = Field(..., description="Target state")
75
+
76
+
77
+ class SearchRequest(BaseModel):
78
+ """Request to search tickets."""
79
+
80
+ query: Optional[str] = Field(None, description="Search query text")
81
+ state: Optional[str] = Field(None, description="Filter by ticket state")
82
+ priority: Optional[str] = Field(None, description="Filter by priority")
83
+ assignee: Optional[str] = Field(None, description="Filter by assignee")
84
+ tags: Optional[list[str]] = Field(None, description="Filter by tags")
85
+ limit: int = Field(10, description="Maximum number of results")
86
+
87
+
88
+ class ListRequest(BaseModel):
89
+ """Request to list tickets."""
90
+
91
+ limit: int = Field(10, description="Maximum number of tickets to return")
92
+ offset: int = Field(0, description="Number of tickets to skip")
93
+ filters: Optional[dict[str, Any]] = Field(None, description="Additional filters")
94
+
95
+
96
+ class DeleteTicketRequest(BaseModel):
97
+ """Request to delete a ticket."""
98
+
99
+ ticket_id: str = Field(..., description="Ticket ID to delete")
100
+
101
+
102
+ class CommentRequest(BaseModel):
103
+ """Request for comment operations."""
104
+
105
+ operation: str = Field("add", description="Operation: 'add' or 'list'")
106
+ ticket_id: str = Field(..., description="Ticket ID")
107
+ content: Optional[str] = Field(None, description="Comment content (for add)")
108
+ author: Optional[str] = Field(None, description="Comment author (for add)")
109
+ limit: int = Field(10, description="Max comments to return (for list)")
110
+ offset: int = Field(0, description="Number of comments to skip (for list)")
111
+
112
+
113
+ class CreatePRRequest(BaseModel):
114
+ """Request to create a pull request."""
115
+
116
+ ticket_id: str = Field(..., description="Ticket ID")
117
+ base_branch: str = Field("main", description="Base branch")
118
+ head_branch: Optional[str] = Field(None, description="Head branch")
119
+ title: Optional[str] = Field(None, description="PR title")
120
+ body: Optional[str] = Field(None, description="PR body")
121
+ draft: bool = Field(False, description="Create as draft PR")
122
+ github_owner: Optional[str] = Field(None, description="GitHub owner (for Linear)")
123
+ github_repo: Optional[str] = Field(None, description="GitHub repo (for Linear)")
124
+
125
+
126
+ class LinkPRRequest(BaseModel):
127
+ """Request to link an existing PR to a ticket."""
128
+
129
+ ticket_id: str = Field(..., description="Ticket ID")
130
+ pr_url: str = Field(..., description="Pull request URL")
131
+
132
+
133
+ class EpicListRequest(BaseModel):
134
+ """Request to list epics."""
135
+
136
+ limit: int = Field(10, description="Maximum number of epics to return")
137
+ offset: int = Field(0, description="Number of epics to skip")
138
+
139
+
140
+ class EpicIssuesRequest(BaseModel):
141
+ """Request to list issues in an epic."""
142
+
143
+ epic_id: str = Field(..., description="Epic ID")
144
+
145
+
146
+ class IssueTasksRequest(BaseModel):
147
+ """Request to list tasks in an issue."""
148
+
149
+ issue_id: str = Field(..., description="Issue ID")
150
+
151
+
152
+ class HierarchyTreeRequest(BaseModel):
153
+ """Request to get hierarchy tree."""
154
+
155
+ epic_id: Optional[str] = Field(None, description="Specific epic ID (optional)")
156
+ max_depth: int = Field(3, description="Maximum depth of tree")
157
+ limit: int = Field(10, description="Max epics to return (if no epic_id)")
158
+
159
+
160
+ class BulkCreateRequest(BaseModel):
161
+ """Request to bulk create tickets."""
162
+
163
+ tickets: list[dict[str, Any]] = Field(..., description="List of ticket data")
164
+
165
+
166
+ class BulkUpdateRequest(BaseModel):
167
+ """Request to bulk update tickets."""
168
+
169
+ updates: list[dict[str, Any]] = Field(..., description="List of update data")
170
+
171
+
172
+ class SearchHierarchyRequest(BaseModel):
173
+ """Request to search with hierarchy context."""
174
+
175
+ query: str = Field("", description="Search query")
176
+ state: Optional[str] = Field(None, description="Filter by state")
177
+ priority: Optional[str] = Field(None, description="Filter by priority")
178
+ include_children: bool = Field(True, description="Include child tickets")
179
+ include_parents: bool = Field(True, description="Include parent tickets")
180
+ limit: int = Field(50, description="Maximum number of results")
181
+
182
+
183
+ class AttachRequest(BaseModel):
184
+ """Request to attach file to ticket."""
185
+
186
+ ticket_id: str = Field(..., description="Ticket ID")
187
+ file_path: Optional[str] = Field(None, description="File path to attach")
188
+ file_content: Optional[str] = Field(None, description="File content (base64)")
189
+ file_name: Optional[str] = Field(None, description="File name")
190
+
191
+
192
+ class ListAttachmentsRequest(BaseModel):
193
+ """Request to list ticket attachments."""
194
+
195
+ ticket_id: str = Field(..., description="Ticket ID")
@@ -0,0 +1,206 @@
1
+ """Response builder utility for consistent MCP responses."""
2
+
3
+ from typing import Any, Optional
4
+
5
+ from .constants import JSONRPC_VERSION, STATUS_COMPLETED
6
+
7
+
8
+ class ResponseBuilder:
9
+ """Build consistent JSON-RPC and MCP responses."""
10
+
11
+ @staticmethod
12
+ def success(
13
+ request_id: Any,
14
+ result: dict[str, Any],
15
+ status: str = STATUS_COMPLETED,
16
+ ) -> dict[str, Any]:
17
+ """Build successful response.
18
+
19
+ Args:
20
+ request_id: Request ID
21
+ result: Result data
22
+ status: Status value
23
+
24
+ Returns:
25
+ JSON-RPC response
26
+
27
+ """
28
+ return {
29
+ "jsonrpc": JSONRPC_VERSION,
30
+ "result": {"status": status, **result},
31
+ "id": request_id,
32
+ }
33
+
34
+ @staticmethod
35
+ def error(
36
+ request_id: Any,
37
+ code: int,
38
+ message: str,
39
+ data: Optional[dict[str, Any]] = None,
40
+ ) -> dict[str, Any]:
41
+ """Build error response.
42
+
43
+ Args:
44
+ request_id: Request ID
45
+ code: Error code
46
+ message: Error message
47
+ data: Additional error data
48
+
49
+ Returns:
50
+ JSON-RPC error response
51
+
52
+ """
53
+ error_obj = {"code": code, "message": message}
54
+ if data:
55
+ error_obj["data"] = data
56
+
57
+ return {
58
+ "jsonrpc": JSONRPC_VERSION,
59
+ "error": error_obj,
60
+ "id": request_id,
61
+ }
62
+
63
+ @staticmethod
64
+ def status_result(status: str, **kwargs: Any) -> dict[str, Any]:
65
+ """Build status result with additional fields.
66
+
67
+ Args:
68
+ status: Status value
69
+ **kwargs: Additional fields
70
+
71
+ Returns:
72
+ Result dictionary
73
+
74
+ """
75
+ return {"status": status, **kwargs}
76
+
77
+ @staticmethod
78
+ def ticket_result(ticket: Any) -> dict[str, Any]:
79
+ """Build ticket result.
80
+
81
+ Args:
82
+ ticket: Ticket object
83
+
84
+ Returns:
85
+ Result dictionary with ticket data
86
+
87
+ """
88
+ return {"ticket": ticket.model_dump()}
89
+
90
+ @staticmethod
91
+ def tickets_result(tickets: list[Any]) -> dict[str, Any]:
92
+ """Build tickets list result.
93
+
94
+ Args:
95
+ tickets: List of ticket objects
96
+
97
+ Returns:
98
+ Result dictionary with tickets data
99
+
100
+ """
101
+ return {"tickets": [t.model_dump() for t in tickets]}
102
+
103
+ @staticmethod
104
+ def comment_result(comment: Any) -> dict[str, Any]:
105
+ """Build comment result.
106
+
107
+ Args:
108
+ comment: Comment object
109
+
110
+ Returns:
111
+ Result dictionary with comment data
112
+
113
+ """
114
+ return {"comment": comment.model_dump()}
115
+
116
+ @staticmethod
117
+ def comments_result(comments: list[Any]) -> dict[str, Any]:
118
+ """Build comments list result.
119
+
120
+ Args:
121
+ comments: List of comment objects
122
+
123
+ Returns:
124
+ Result dictionary with comments data
125
+
126
+ """
127
+ return {"comments": [c.model_dump() for c in comments]}
128
+
129
+ @staticmethod
130
+ def deletion_result(ticket_id: str, success: bool) -> dict[str, Any]:
131
+ """Build deletion result.
132
+
133
+ Args:
134
+ ticket_id: ID of deleted ticket
135
+ success: Whether deletion was successful
136
+
137
+ Returns:
138
+ Result dictionary with deletion status
139
+
140
+ """
141
+ return {"success": success, "ticket_id": ticket_id}
142
+
143
+ @staticmethod
144
+ def bulk_result(results: list[dict[str, Any]]) -> dict[str, Any]:
145
+ """Build bulk operation result.
146
+
147
+ Args:
148
+ results: List of operation results
149
+
150
+ Returns:
151
+ Result dictionary with bulk operation data
152
+
153
+ """
154
+ return {"results": results, "count": len(results)}
155
+
156
+ @staticmethod
157
+ def epics_result(epics: list[Any]) -> dict[str, Any]:
158
+ """Build epics list result.
159
+
160
+ Args:
161
+ epics: List of epic objects
162
+
163
+ Returns:
164
+ Result dictionary with epics data
165
+
166
+ """
167
+ return {"epics": [epic.model_dump() for epic in epics]}
168
+
169
+ @staticmethod
170
+ def issues_result(issues: list[Any]) -> dict[str, Any]:
171
+ """Build issues list result.
172
+
173
+ Args:
174
+ issues: List of issue objects
175
+
176
+ Returns:
177
+ Result dictionary with issues data
178
+
179
+ """
180
+ return {"issues": [issue.model_dump() for issue in issues]}
181
+
182
+ @staticmethod
183
+ def tasks_result(tasks: list[Any]) -> dict[str, Any]:
184
+ """Build tasks list result.
185
+
186
+ Args:
187
+ tasks: List of task objects
188
+
189
+ Returns:
190
+ Result dictionary with tasks data
191
+
192
+ """
193
+ return {"tasks": [task.model_dump() for task in tasks]}
194
+
195
+ @staticmethod
196
+ def attachments_result(attachments: list[Any]) -> dict[str, Any]:
197
+ """Build attachments list result.
198
+
199
+ Args:
200
+ attachments: List of attachment objects
201
+
202
+ Returns:
203
+ Result dictionary with attachments data
204
+
205
+ """
206
+ return {"attachments": attachments}