mcp-ticketer 0.1.21__py3-none-any.whl → 0.1.23__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 +7 -7
- mcp_ticketer/__version__.py +4 -2
- mcp_ticketer/adapters/__init__.py +4 -4
- mcp_ticketer/adapters/aitrackdown.py +66 -49
- mcp_ticketer/adapters/github.py +192 -125
- mcp_ticketer/adapters/hybrid.py +99 -53
- mcp_ticketer/adapters/jira.py +161 -151
- mcp_ticketer/adapters/linear.py +396 -246
- mcp_ticketer/cache/__init__.py +1 -1
- mcp_ticketer/cache/memory.py +15 -16
- mcp_ticketer/cli/__init__.py +1 -1
- mcp_ticketer/cli/configure.py +69 -93
- mcp_ticketer/cli/discover.py +43 -35
- mcp_ticketer/cli/main.py +283 -298
- mcp_ticketer/cli/mcp_configure.py +39 -15
- mcp_ticketer/cli/migrate_config.py +11 -13
- mcp_ticketer/cli/queue_commands.py +21 -58
- mcp_ticketer/cli/utils.py +121 -66
- mcp_ticketer/core/__init__.py +2 -2
- mcp_ticketer/core/adapter.py +46 -39
- mcp_ticketer/core/config.py +128 -92
- mcp_ticketer/core/env_discovery.py +69 -37
- mcp_ticketer/core/http_client.py +57 -40
- mcp_ticketer/core/mappers.py +98 -54
- mcp_ticketer/core/models.py +38 -24
- mcp_ticketer/core/project_config.py +145 -80
- mcp_ticketer/core/registry.py +16 -16
- mcp_ticketer/mcp/__init__.py +1 -1
- mcp_ticketer/mcp/server.py +199 -145
- mcp_ticketer/queue/__init__.py +2 -2
- mcp_ticketer/queue/__main__.py +1 -1
- mcp_ticketer/queue/manager.py +30 -26
- mcp_ticketer/queue/queue.py +147 -85
- mcp_ticketer/queue/run_worker.py +2 -3
- mcp_ticketer/queue/worker.py +55 -40
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/METADATA +1 -1
- mcp_ticketer-0.1.23.dist-info/RECORD +42 -0
- mcp_ticketer-0.1.21.dist-info/RECORD +0 -42
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/top_level.txt +0 -0
mcp_ticketer/mcp/server.py
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
5
|
import sys
|
|
6
|
-
from typing import Any, Dict, List, Optional
|
|
7
6
|
from pathlib import Path
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
8
9
|
from dotenv import load_dotenv
|
|
9
10
|
|
|
10
|
-
from ..core import
|
|
11
|
-
from ..core.models import SearchQuery
|
|
12
|
-
from ..adapters import AITrackdownAdapter
|
|
11
|
+
from ..core import AdapterRegistry
|
|
12
|
+
from ..core.models import SearchQuery
|
|
13
13
|
from ..queue import Queue, QueueStatus, WorkerManager
|
|
14
14
|
|
|
15
15
|
# Load environment variables early (prioritize .env.local)
|
|
@@ -33,20 +33,22 @@ else:
|
|
|
33
33
|
class MCPTicketServer:
|
|
34
34
|
"""MCP server for ticket operations over stdio."""
|
|
35
35
|
|
|
36
|
-
def __init__(
|
|
36
|
+
def __init__(
|
|
37
|
+
self, adapter_type: str = "aitrackdown", config: Optional[dict[str, Any]] = None
|
|
38
|
+
):
|
|
37
39
|
"""Initialize MCP server.
|
|
38
40
|
|
|
39
41
|
Args:
|
|
40
42
|
adapter_type: Type of adapter to use
|
|
41
43
|
config: Adapter configuration
|
|
44
|
+
|
|
42
45
|
"""
|
|
43
46
|
self.adapter = AdapterRegistry.get_adapter(
|
|
44
|
-
adapter_type,
|
|
45
|
-
config or {"base_path": ".aitrackdown"}
|
|
47
|
+
adapter_type, config or {"base_path": ".aitrackdown"}
|
|
46
48
|
)
|
|
47
49
|
self.running = False
|
|
48
50
|
|
|
49
|
-
async def handle_request(self, request:
|
|
51
|
+
async def handle_request(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
50
52
|
"""Handle JSON-RPC request.
|
|
51
53
|
|
|
52
54
|
Args:
|
|
@@ -54,6 +56,7 @@ class MCPTicketServer:
|
|
|
54
56
|
|
|
55
57
|
Returns:
|
|
56
58
|
JSON-RPC response
|
|
59
|
+
|
|
57
60
|
"""
|
|
58
61
|
method = request.get("method")
|
|
59
62
|
params = request.get("params", {})
|
|
@@ -92,30 +95,17 @@ class MCPTicketServer:
|
|
|
92
95
|
result = await self._handle_tools_call(params)
|
|
93
96
|
else:
|
|
94
97
|
return self._error_response(
|
|
95
|
-
request_id,
|
|
96
|
-
-32601,
|
|
97
|
-
f"Method not found: {method}"
|
|
98
|
+
request_id, -32601, f"Method not found: {method}"
|
|
98
99
|
)
|
|
99
100
|
|
|
100
|
-
return {
|
|
101
|
-
"jsonrpc": "2.0",
|
|
102
|
-
"result": result,
|
|
103
|
-
"id": request_id
|
|
104
|
-
}
|
|
101
|
+
return {"jsonrpc": "2.0", "result": result, "id": request_id}
|
|
105
102
|
|
|
106
103
|
except Exception as e:
|
|
107
|
-
return self._error_response(
|
|
108
|
-
request_id,
|
|
109
|
-
-32603,
|
|
110
|
-
f"Internal error: {str(e)}"
|
|
111
|
-
)
|
|
104
|
+
return self._error_response(request_id, -32603, f"Internal error: {str(e)}")
|
|
112
105
|
|
|
113
106
|
def _error_response(
|
|
114
|
-
self,
|
|
115
|
-
|
|
116
|
-
code: int,
|
|
117
|
-
message: str
|
|
118
|
-
) -> Dict[str, Any]:
|
|
107
|
+
self, request_id: Any, code: int, message: str
|
|
108
|
+
) -> dict[str, Any]:
|
|
119
109
|
"""Create error response.
|
|
120
110
|
|
|
121
111
|
Args:
|
|
@@ -125,17 +115,15 @@ class MCPTicketServer:
|
|
|
125
115
|
|
|
126
116
|
Returns:
|
|
127
117
|
Error response
|
|
118
|
+
|
|
128
119
|
"""
|
|
129
120
|
return {
|
|
130
121
|
"jsonrpc": "2.0",
|
|
131
|
-
"error": {
|
|
132
|
-
|
|
133
|
-
"message": message
|
|
134
|
-
},
|
|
135
|
-
"id": request_id
|
|
122
|
+
"error": {"code": code, "message": message},
|
|
123
|
+
"id": request_id,
|
|
136
124
|
}
|
|
137
125
|
|
|
138
|
-
async def _handle_create(self, params:
|
|
126
|
+
async def _handle_create(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
139
127
|
"""Handle ticket creation."""
|
|
140
128
|
# Queue the operation instead of direct execution
|
|
141
129
|
queue = Queue()
|
|
@@ -150,7 +138,7 @@ class MCPTicketServer:
|
|
|
150
138
|
queue_id = queue.add(
|
|
151
139
|
ticket_data=task_data,
|
|
152
140
|
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
153
|
-
operation="create"
|
|
141
|
+
operation="create",
|
|
154
142
|
)
|
|
155
143
|
|
|
156
144
|
# Start worker if needed
|
|
@@ -162,7 +150,7 @@ class MCPTicketServer:
|
|
|
162
150
|
return {
|
|
163
151
|
"queue_id": queue_id,
|
|
164
152
|
"status": "queued",
|
|
165
|
-
"message": f"Ticket creation queued with ID: {queue_id}"
|
|
153
|
+
"message": f"Ticket creation queued with ID: {queue_id}",
|
|
166
154
|
}
|
|
167
155
|
|
|
168
156
|
# Poll for completion with timeout (default synchronous behavior)
|
|
@@ -178,7 +166,7 @@ class MCPTicketServer:
|
|
|
178
166
|
return {
|
|
179
167
|
"queue_id": queue_id,
|
|
180
168
|
"status": "error",
|
|
181
|
-
"error": f"Queue item {queue_id} not found"
|
|
169
|
+
"error": f"Queue item {queue_id} not found",
|
|
182
170
|
}
|
|
183
171
|
|
|
184
172
|
# If completed, return with ticket ID
|
|
@@ -186,7 +174,7 @@ class MCPTicketServer:
|
|
|
186
174
|
response = {
|
|
187
175
|
"queue_id": queue_id,
|
|
188
176
|
"status": "completed",
|
|
189
|
-
"title": params["title"]
|
|
177
|
+
"title": params["title"],
|
|
190
178
|
}
|
|
191
179
|
|
|
192
180
|
# Add ticket ID and other result data if available
|
|
@@ -197,9 +185,13 @@ class MCPTicketServer:
|
|
|
197
185
|
# Try to construct URL if we have enough information
|
|
198
186
|
if response.get("ticket_id"):
|
|
199
187
|
# This is adapter-specific, but we can add URL generation later
|
|
200
|
-
response["id"] = response[
|
|
188
|
+
response["id"] = response[
|
|
189
|
+
"ticket_id"
|
|
190
|
+
] # Also include as "id" for compatibility
|
|
201
191
|
|
|
202
|
-
response["message"] =
|
|
192
|
+
response["message"] = (
|
|
193
|
+
f"Ticket created successfully: {response.get('ticket_id', queue_id)}"
|
|
194
|
+
)
|
|
203
195
|
return response
|
|
204
196
|
|
|
205
197
|
# If failed, return error
|
|
@@ -208,7 +200,7 @@ class MCPTicketServer:
|
|
|
208
200
|
"queue_id": queue_id,
|
|
209
201
|
"status": "failed",
|
|
210
202
|
"error": item.error_message or "Ticket creation failed",
|
|
211
|
-
"title": params["title"]
|
|
203
|
+
"title": params["title"],
|
|
212
204
|
}
|
|
213
205
|
|
|
214
206
|
# Check timeout
|
|
@@ -218,18 +210,18 @@ class MCPTicketServer:
|
|
|
218
210
|
"queue_id": queue_id,
|
|
219
211
|
"status": "timeout",
|
|
220
212
|
"message": f"Ticket creation timed out after {max_wait_time} seconds. Use ticket_status with queue_id to check status.",
|
|
221
|
-
"title": params["title"]
|
|
213
|
+
"title": params["title"],
|
|
222
214
|
}
|
|
223
215
|
|
|
224
216
|
# Wait before next poll
|
|
225
217
|
await asyncio.sleep(poll_interval)
|
|
226
218
|
|
|
227
|
-
async def _handle_read(self, params:
|
|
219
|
+
async def _handle_read(self, params: dict[str, Any]) -> Optional[dict[str, Any]]:
|
|
228
220
|
"""Handle ticket read."""
|
|
229
221
|
ticket = await self.adapter.read(params["ticket_id"])
|
|
230
222
|
return ticket.model_dump() if ticket else None
|
|
231
223
|
|
|
232
|
-
async def _handle_update(self, params:
|
|
224
|
+
async def _handle_update(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
233
225
|
"""Handle ticket update."""
|
|
234
226
|
# Queue the operation
|
|
235
227
|
queue = Queue()
|
|
@@ -239,7 +231,7 @@ class MCPTicketServer:
|
|
|
239
231
|
queue_id = queue.add(
|
|
240
232
|
ticket_data=updates,
|
|
241
233
|
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
242
|
-
operation="update"
|
|
234
|
+
operation="update",
|
|
243
235
|
)
|
|
244
236
|
|
|
245
237
|
# Start worker if needed
|
|
@@ -259,7 +251,7 @@ class MCPTicketServer:
|
|
|
259
251
|
return {
|
|
260
252
|
"queue_id": queue_id,
|
|
261
253
|
"status": "error",
|
|
262
|
-
"error": f"Queue item {queue_id} not found"
|
|
254
|
+
"error": f"Queue item {queue_id} not found",
|
|
263
255
|
}
|
|
264
256
|
|
|
265
257
|
# If completed, return with ticket ID
|
|
@@ -267,7 +259,7 @@ class MCPTicketServer:
|
|
|
267
259
|
response = {
|
|
268
260
|
"queue_id": queue_id,
|
|
269
261
|
"status": "completed",
|
|
270
|
-
"ticket_id": params["ticket_id"]
|
|
262
|
+
"ticket_id": params["ticket_id"],
|
|
271
263
|
}
|
|
272
264
|
|
|
273
265
|
# Add result data if available
|
|
@@ -276,7 +268,9 @@ class MCPTicketServer:
|
|
|
276
268
|
response["ticket_id"] = item.result["id"]
|
|
277
269
|
response["success"] = item.result.get("success", True)
|
|
278
270
|
|
|
279
|
-
response["message"] =
|
|
271
|
+
response["message"] = (
|
|
272
|
+
f"Ticket updated successfully: {response['ticket_id']}"
|
|
273
|
+
)
|
|
280
274
|
return response
|
|
281
275
|
|
|
282
276
|
# If failed, return error
|
|
@@ -285,7 +279,7 @@ class MCPTicketServer:
|
|
|
285
279
|
"queue_id": queue_id,
|
|
286
280
|
"status": "failed",
|
|
287
281
|
"error": item.error_message or "Ticket update failed",
|
|
288
|
-
"ticket_id": params["ticket_id"]
|
|
282
|
+
"ticket_id": params["ticket_id"],
|
|
289
283
|
}
|
|
290
284
|
|
|
291
285
|
# Check timeout
|
|
@@ -295,20 +289,20 @@ class MCPTicketServer:
|
|
|
295
289
|
"queue_id": queue_id,
|
|
296
290
|
"status": "timeout",
|
|
297
291
|
"message": f"Ticket update timed out after {max_wait_time} seconds. Use ticket_status with queue_id to check status.",
|
|
298
|
-
"ticket_id": params["ticket_id"]
|
|
292
|
+
"ticket_id": params["ticket_id"],
|
|
299
293
|
}
|
|
300
294
|
|
|
301
295
|
# Wait before next poll
|
|
302
296
|
await asyncio.sleep(poll_interval)
|
|
303
297
|
|
|
304
|
-
async def _handle_delete(self, params:
|
|
298
|
+
async def _handle_delete(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
305
299
|
"""Handle ticket deletion."""
|
|
306
300
|
# Queue the operation
|
|
307
301
|
queue = Queue()
|
|
308
302
|
queue_id = queue.add(
|
|
309
303
|
ticket_data={"ticket_id": params["ticket_id"]},
|
|
310
304
|
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
311
|
-
operation="delete"
|
|
305
|
+
operation="delete",
|
|
312
306
|
)
|
|
313
307
|
|
|
314
308
|
# Start worker if needed
|
|
@@ -318,35 +312,35 @@ class MCPTicketServer:
|
|
|
318
312
|
return {
|
|
319
313
|
"queue_id": queue_id,
|
|
320
314
|
"status": "queued",
|
|
321
|
-
"message": f"Ticket deletion queued with ID: {queue_id}"
|
|
315
|
+
"message": f"Ticket deletion queued with ID: {queue_id}",
|
|
322
316
|
}
|
|
323
317
|
|
|
324
|
-
async def _handle_list(self, params:
|
|
318
|
+
async def _handle_list(self, params: dict[str, Any]) -> list[dict[str, Any]]:
|
|
325
319
|
"""Handle ticket listing."""
|
|
326
320
|
tickets = await self.adapter.list(
|
|
327
321
|
limit=params.get("limit", 10),
|
|
328
322
|
offset=params.get("offset", 0),
|
|
329
|
-
filters=params.get("filters")
|
|
323
|
+
filters=params.get("filters"),
|
|
330
324
|
)
|
|
331
325
|
return [ticket.model_dump() for ticket in tickets]
|
|
332
326
|
|
|
333
|
-
async def _handle_search(self, params:
|
|
327
|
+
async def _handle_search(self, params: dict[str, Any]) -> list[dict[str, Any]]:
|
|
334
328
|
"""Handle ticket search."""
|
|
335
329
|
query = SearchQuery(**params)
|
|
336
330
|
tickets = await self.adapter.search(query)
|
|
337
331
|
return [ticket.model_dump() for ticket in tickets]
|
|
338
332
|
|
|
339
|
-
async def _handle_transition(self, params:
|
|
333
|
+
async def _handle_transition(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
340
334
|
"""Handle state transition."""
|
|
341
335
|
# Queue the operation
|
|
342
336
|
queue = Queue()
|
|
343
337
|
queue_id = queue.add(
|
|
344
338
|
ticket_data={
|
|
345
339
|
"ticket_id": params["ticket_id"],
|
|
346
|
-
"state": params["target_state"]
|
|
340
|
+
"state": params["target_state"],
|
|
347
341
|
},
|
|
348
342
|
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
349
|
-
operation="transition"
|
|
343
|
+
operation="transition",
|
|
350
344
|
)
|
|
351
345
|
|
|
352
346
|
# Start worker if needed
|
|
@@ -366,7 +360,7 @@ class MCPTicketServer:
|
|
|
366
360
|
return {
|
|
367
361
|
"queue_id": queue_id,
|
|
368
362
|
"status": "error",
|
|
369
|
-
"error": f"Queue item {queue_id} not found"
|
|
363
|
+
"error": f"Queue item {queue_id} not found",
|
|
370
364
|
}
|
|
371
365
|
|
|
372
366
|
# If completed, return with ticket ID
|
|
@@ -375,7 +369,7 @@ class MCPTicketServer:
|
|
|
375
369
|
"queue_id": queue_id,
|
|
376
370
|
"status": "completed",
|
|
377
371
|
"ticket_id": params["ticket_id"],
|
|
378
|
-
"state": params["target_state"]
|
|
372
|
+
"state": params["target_state"],
|
|
379
373
|
}
|
|
380
374
|
|
|
381
375
|
# Add result data if available
|
|
@@ -384,7 +378,9 @@ class MCPTicketServer:
|
|
|
384
378
|
response["ticket_id"] = item.result["id"]
|
|
385
379
|
response["success"] = item.result.get("success", True)
|
|
386
380
|
|
|
387
|
-
response["message"] =
|
|
381
|
+
response["message"] = (
|
|
382
|
+
f"State transition completed successfully: {response['ticket_id']} → {params['target_state']}"
|
|
383
|
+
)
|
|
388
384
|
return response
|
|
389
385
|
|
|
390
386
|
# If failed, return error
|
|
@@ -393,7 +389,7 @@ class MCPTicketServer:
|
|
|
393
389
|
"queue_id": queue_id,
|
|
394
390
|
"status": "failed",
|
|
395
391
|
"error": item.error_message or "State transition failed",
|
|
396
|
-
"ticket_id": params["ticket_id"]
|
|
392
|
+
"ticket_id": params["ticket_id"],
|
|
397
393
|
}
|
|
398
394
|
|
|
399
395
|
# Check timeout
|
|
@@ -403,13 +399,13 @@ class MCPTicketServer:
|
|
|
403
399
|
"queue_id": queue_id,
|
|
404
400
|
"status": "timeout",
|
|
405
401
|
"message": f"State transition timed out after {max_wait_time} seconds. Use ticket_status with queue_id to check status.",
|
|
406
|
-
"ticket_id": params["ticket_id"]
|
|
402
|
+
"ticket_id": params["ticket_id"],
|
|
407
403
|
}
|
|
408
404
|
|
|
409
405
|
# Wait before next poll
|
|
410
406
|
await asyncio.sleep(poll_interval)
|
|
411
407
|
|
|
412
|
-
async def _handle_comment(self, params:
|
|
408
|
+
async def _handle_comment(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
413
409
|
"""Handle comment operations."""
|
|
414
410
|
operation = params.get("operation", "add")
|
|
415
411
|
|
|
@@ -420,10 +416,10 @@ class MCPTicketServer:
|
|
|
420
416
|
ticket_data={
|
|
421
417
|
"ticket_id": params["ticket_id"],
|
|
422
418
|
"content": params["content"],
|
|
423
|
-
"author": params.get("author")
|
|
419
|
+
"author": params.get("author"),
|
|
424
420
|
},
|
|
425
421
|
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
426
|
-
operation="comment"
|
|
422
|
+
operation="comment",
|
|
427
423
|
)
|
|
428
424
|
|
|
429
425
|
# Start worker if needed
|
|
@@ -433,7 +429,7 @@ class MCPTicketServer:
|
|
|
433
429
|
return {
|
|
434
430
|
"queue_id": queue_id,
|
|
435
431
|
"status": "queued",
|
|
436
|
-
"message": f"Comment addition queued with ID: {queue_id}"
|
|
432
|
+
"message": f"Comment addition queued with ID: {queue_id}",
|
|
437
433
|
}
|
|
438
434
|
|
|
439
435
|
elif operation == "list":
|
|
@@ -441,14 +437,14 @@ class MCPTicketServer:
|
|
|
441
437
|
comments = await self.adapter.get_comments(
|
|
442
438
|
params["ticket_id"],
|
|
443
439
|
limit=params.get("limit", 10),
|
|
444
|
-
offset=params.get("offset", 0)
|
|
440
|
+
offset=params.get("offset", 0),
|
|
445
441
|
)
|
|
446
442
|
return [comment.model_dump() for comment in comments]
|
|
447
443
|
|
|
448
444
|
else:
|
|
449
445
|
raise ValueError(f"Unknown comment operation: {operation}")
|
|
450
446
|
|
|
451
|
-
async def _handle_queue_status(self, params:
|
|
447
|
+
async def _handle_queue_status(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
452
448
|
"""Check status of queued operation."""
|
|
453
449
|
queue_id = params.get("queue_id")
|
|
454
450
|
if not queue_id:
|
|
@@ -458,16 +454,14 @@ class MCPTicketServer:
|
|
|
458
454
|
item = queue.get_item(queue_id)
|
|
459
455
|
|
|
460
456
|
if not item:
|
|
461
|
-
return {
|
|
462
|
-
"error": f"Queue item not found: {queue_id}"
|
|
463
|
-
}
|
|
457
|
+
return {"error": f"Queue item not found: {queue_id}"}
|
|
464
458
|
|
|
465
459
|
response = {
|
|
466
460
|
"queue_id": item.id,
|
|
467
461
|
"status": item.status.value,
|
|
468
462
|
"operation": item.operation,
|
|
469
463
|
"created_at": item.created_at.isoformat(),
|
|
470
|
-
"retry_count": item.retry_count
|
|
464
|
+
"retry_count": item.retry_count,
|
|
471
465
|
}
|
|
472
466
|
|
|
473
467
|
if item.processed_at:
|
|
@@ -481,7 +475,7 @@ class MCPTicketServer:
|
|
|
481
475
|
|
|
482
476
|
return response
|
|
483
477
|
|
|
484
|
-
async def _handle_create_pr(self, params:
|
|
478
|
+
async def _handle_create_pr(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
485
479
|
"""Handle PR creation for a ticket."""
|
|
486
480
|
ticket_id = params.get("ticket_id")
|
|
487
481
|
if not ticket_id:
|
|
@@ -493,6 +487,7 @@ class MCPTicketServer:
|
|
|
493
487
|
if "github" in adapter_name:
|
|
494
488
|
# GitHub adapter supports direct PR creation
|
|
495
489
|
from ..adapters.github import GitHubAdapter
|
|
490
|
+
|
|
496
491
|
if isinstance(self.adapter, GitHubAdapter):
|
|
497
492
|
try:
|
|
498
493
|
result = await self.adapter.create_pull_request(
|
|
@@ -520,6 +515,7 @@ class MCPTicketServer:
|
|
|
520
515
|
elif "linear" in adapter_name:
|
|
521
516
|
# Linear adapter needs GitHub config for PR creation
|
|
522
517
|
from ..adapters.linear import LinearAdapter
|
|
518
|
+
|
|
523
519
|
if isinstance(self.adapter, LinearAdapter):
|
|
524
520
|
# For Linear, we prepare the branch and metadata but can't create the actual PR
|
|
525
521
|
# without GitHub integration configured
|
|
@@ -567,7 +563,7 @@ class MCPTicketServer:
|
|
|
567
563
|
"ticket_id": ticket_id,
|
|
568
564
|
}
|
|
569
565
|
|
|
570
|
-
async def _handle_link_pr(self, params:
|
|
566
|
+
async def _handle_link_pr(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
571
567
|
"""Handle linking an existing PR to a ticket."""
|
|
572
568
|
ticket_id = params.get("ticket_id")
|
|
573
569
|
pr_url = params.get("pr_url")
|
|
@@ -581,6 +577,7 @@ class MCPTicketServer:
|
|
|
581
577
|
|
|
582
578
|
if "github" in adapter_name:
|
|
583
579
|
from ..adapters.github import GitHubAdapter
|
|
580
|
+
|
|
584
581
|
if isinstance(self.adapter, GitHubAdapter):
|
|
585
582
|
try:
|
|
586
583
|
result = await self.adapter.link_existing_pull_request(
|
|
@@ -597,6 +594,7 @@ class MCPTicketServer:
|
|
|
597
594
|
}
|
|
598
595
|
elif "linear" in adapter_name:
|
|
599
596
|
from ..adapters.linear import LinearAdapter
|
|
597
|
+
|
|
600
598
|
if isinstance(self.adapter, LinearAdapter):
|
|
601
599
|
try:
|
|
602
600
|
result = await self.adapter.link_to_pull_request(
|
|
@@ -619,7 +617,7 @@ class MCPTicketServer:
|
|
|
619
617
|
"pr_url": pr_url,
|
|
620
618
|
}
|
|
621
619
|
|
|
622
|
-
async def _handle_initialize(self, params:
|
|
620
|
+
async def _handle_initialize(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
623
621
|
"""Handle initialize request from MCP client.
|
|
624
622
|
|
|
625
623
|
Args:
|
|
@@ -627,21 +625,15 @@ class MCPTicketServer:
|
|
|
627
625
|
|
|
628
626
|
Returns:
|
|
629
627
|
Server capabilities
|
|
628
|
+
|
|
630
629
|
"""
|
|
631
630
|
return {
|
|
632
631
|
"protocolVersion": "2024-11-05",
|
|
633
|
-
"serverInfo": {
|
|
634
|
-
|
|
635
|
-
"version": "0.1.8"
|
|
636
|
-
},
|
|
637
|
-
"capabilities": {
|
|
638
|
-
"tools": {
|
|
639
|
-
"listChanged": False
|
|
640
|
-
}
|
|
641
|
-
}
|
|
632
|
+
"serverInfo": {"name": "mcp-ticketer", "version": "0.1.8"},
|
|
633
|
+
"capabilities": {"tools": {"listChanged": False}},
|
|
642
634
|
}
|
|
643
635
|
|
|
644
|
-
async def _handle_tools_list(self) ->
|
|
636
|
+
async def _handle_tools_list(self) -> dict[str, Any]:
|
|
645
637
|
"""List available MCP tools."""
|
|
646
638
|
return {
|
|
647
639
|
"tools": [
|
|
@@ -651,15 +643,35 @@ class MCPTicketServer:
|
|
|
651
643
|
"inputSchema": {
|
|
652
644
|
"type": "object",
|
|
653
645
|
"properties": {
|
|
654
|
-
"ticket_id": {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
"
|
|
659
|
-
|
|
646
|
+
"ticket_id": {
|
|
647
|
+
"type": "string",
|
|
648
|
+
"description": "Ticket ID to link the PR to",
|
|
649
|
+
},
|
|
650
|
+
"base_branch": {
|
|
651
|
+
"type": "string",
|
|
652
|
+
"description": "Target branch for the PR",
|
|
653
|
+
"default": "main",
|
|
654
|
+
},
|
|
655
|
+
"head_branch": {
|
|
656
|
+
"type": "string",
|
|
657
|
+
"description": "Source branch name (auto-generated if not provided)",
|
|
658
|
+
},
|
|
659
|
+
"title": {
|
|
660
|
+
"type": "string",
|
|
661
|
+
"description": "PR title (uses ticket title if not provided)",
|
|
662
|
+
},
|
|
663
|
+
"body": {
|
|
664
|
+
"type": "string",
|
|
665
|
+
"description": "PR description (auto-generated with issue link if not provided)",
|
|
666
|
+
},
|
|
667
|
+
"draft": {
|
|
668
|
+
"type": "boolean",
|
|
669
|
+
"description": "Create as draft PR",
|
|
670
|
+
"default": False,
|
|
671
|
+
},
|
|
660
672
|
},
|
|
661
|
-
"required": ["ticket_id"]
|
|
662
|
-
}
|
|
673
|
+
"required": ["ticket_id"],
|
|
674
|
+
},
|
|
663
675
|
},
|
|
664
676
|
{
|
|
665
677
|
"name": "ticket_link_pr",
|
|
@@ -667,11 +679,17 @@ class MCPTicketServer:
|
|
|
667
679
|
"inputSchema": {
|
|
668
680
|
"type": "object",
|
|
669
681
|
"properties": {
|
|
670
|
-
"ticket_id": {
|
|
671
|
-
|
|
682
|
+
"ticket_id": {
|
|
683
|
+
"type": "string",
|
|
684
|
+
"description": "Ticket ID to link the PR to",
|
|
685
|
+
},
|
|
686
|
+
"pr_url": {
|
|
687
|
+
"type": "string",
|
|
688
|
+
"description": "GitHub PR URL to link",
|
|
689
|
+
},
|
|
672
690
|
},
|
|
673
|
-
"required": ["ticket_id", "pr_url"]
|
|
674
|
-
}
|
|
691
|
+
"required": ["ticket_id", "pr_url"],
|
|
692
|
+
},
|
|
675
693
|
},
|
|
676
694
|
{
|
|
677
695
|
"name": "ticket_create",
|
|
@@ -680,13 +698,19 @@ class MCPTicketServer:
|
|
|
680
698
|
"type": "object",
|
|
681
699
|
"properties": {
|
|
682
700
|
"title": {"type": "string", "description": "Ticket title"},
|
|
683
|
-
"description": {
|
|
684
|
-
|
|
701
|
+
"description": {
|
|
702
|
+
"type": "string",
|
|
703
|
+
"description": "Description",
|
|
704
|
+
},
|
|
705
|
+
"priority": {
|
|
706
|
+
"type": "string",
|
|
707
|
+
"enum": ["low", "medium", "high", "critical"],
|
|
708
|
+
},
|
|
685
709
|
"tags": {"type": "array", "items": {"type": "string"}},
|
|
686
710
|
"assignee": {"type": "string"},
|
|
687
711
|
},
|
|
688
|
-
"required": ["title"]
|
|
689
|
-
}
|
|
712
|
+
"required": ["title"],
|
|
713
|
+
},
|
|
690
714
|
},
|
|
691
715
|
{
|
|
692
716
|
"name": "ticket_list",
|
|
@@ -697,8 +721,8 @@ class MCPTicketServer:
|
|
|
697
721
|
"limit": {"type": "integer", "default": 10},
|
|
698
722
|
"state": {"type": "string"},
|
|
699
723
|
"priority": {"type": "string"},
|
|
700
|
-
}
|
|
701
|
-
}
|
|
724
|
+
},
|
|
725
|
+
},
|
|
702
726
|
},
|
|
703
727
|
{
|
|
704
728
|
"name": "ticket_update",
|
|
@@ -707,10 +731,13 @@ class MCPTicketServer:
|
|
|
707
731
|
"type": "object",
|
|
708
732
|
"properties": {
|
|
709
733
|
"ticket_id": {"type": "string", "description": "Ticket ID"},
|
|
710
|
-
"updates": {
|
|
734
|
+
"updates": {
|
|
735
|
+
"type": "object",
|
|
736
|
+
"description": "Fields to update",
|
|
737
|
+
},
|
|
711
738
|
},
|
|
712
|
-
"required": ["ticket_id", "updates"]
|
|
713
|
-
}
|
|
739
|
+
"required": ["ticket_id", "updates"],
|
|
740
|
+
},
|
|
714
741
|
},
|
|
715
742
|
{
|
|
716
743
|
"name": "ticket_transition",
|
|
@@ -721,8 +748,8 @@ class MCPTicketServer:
|
|
|
721
748
|
"ticket_id": {"type": "string"},
|
|
722
749
|
"target_state": {"type": "string"},
|
|
723
750
|
},
|
|
724
|
-
"required": ["ticket_id", "target_state"]
|
|
725
|
-
}
|
|
751
|
+
"required": ["ticket_id", "target_state"],
|
|
752
|
+
},
|
|
726
753
|
},
|
|
727
754
|
{
|
|
728
755
|
"name": "ticket_search",
|
|
@@ -734,8 +761,8 @@ class MCPTicketServer:
|
|
|
734
761
|
"state": {"type": "string"},
|
|
735
762
|
"priority": {"type": "string"},
|
|
736
763
|
"limit": {"type": "integer", "default": 10},
|
|
737
|
-
}
|
|
738
|
-
}
|
|
764
|
+
},
|
|
765
|
+
},
|
|
739
766
|
},
|
|
740
767
|
{
|
|
741
768
|
"name": "ticket_status",
|
|
@@ -743,15 +770,18 @@ class MCPTicketServer:
|
|
|
743
770
|
"inputSchema": {
|
|
744
771
|
"type": "object",
|
|
745
772
|
"properties": {
|
|
746
|
-
"queue_id": {
|
|
773
|
+
"queue_id": {
|
|
774
|
+
"type": "string",
|
|
775
|
+
"description": "Queue ID returned from create/update/delete operations",
|
|
776
|
+
},
|
|
747
777
|
},
|
|
748
|
-
"required": ["queue_id"]
|
|
749
|
-
}
|
|
778
|
+
"required": ["queue_id"],
|
|
779
|
+
},
|
|
750
780
|
},
|
|
751
781
|
]
|
|
752
782
|
}
|
|
753
783
|
|
|
754
|
-
async def _handle_tools_call(self, params:
|
|
784
|
+
async def _handle_tools_call(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
755
785
|
"""Handle tool invocation from MCP client.
|
|
756
786
|
|
|
757
787
|
Args:
|
|
@@ -759,6 +789,7 @@ class MCPTicketServer:
|
|
|
759
789
|
|
|
760
790
|
Returns:
|
|
761
791
|
MCP formatted response with content array
|
|
792
|
+
|
|
762
793
|
"""
|
|
763
794
|
tool_name = params.get("name")
|
|
764
795
|
arguments = params.get("arguments", {})
|
|
@@ -783,13 +814,8 @@ class MCPTicketServer:
|
|
|
783
814
|
result = await self._handle_link_pr(arguments)
|
|
784
815
|
else:
|
|
785
816
|
return {
|
|
786
|
-
"content": [
|
|
787
|
-
|
|
788
|
-
"type": "text",
|
|
789
|
-
"text": f"Unknown tool: {tool_name}"
|
|
790
|
-
}
|
|
791
|
-
],
|
|
792
|
-
"isError": True
|
|
817
|
+
"content": [{"type": "text", "text": f"Unknown tool: {tool_name}"}],
|
|
818
|
+
"isError": True,
|
|
793
819
|
}
|
|
794
820
|
|
|
795
821
|
# Format successful response in MCP content format
|
|
@@ -804,13 +830,8 @@ class MCPTicketServer:
|
|
|
804
830
|
result_text = str(result)
|
|
805
831
|
|
|
806
832
|
return {
|
|
807
|
-
"content": [
|
|
808
|
-
|
|
809
|
-
"type": "text",
|
|
810
|
-
"text": result_text
|
|
811
|
-
}
|
|
812
|
-
],
|
|
813
|
-
"isError": False
|
|
833
|
+
"content": [{"type": "text", "text": result_text}],
|
|
834
|
+
"isError": False,
|
|
814
835
|
}
|
|
815
836
|
|
|
816
837
|
except Exception as e:
|
|
@@ -819,10 +840,10 @@ class MCPTicketServer:
|
|
|
819
840
|
"content": [
|
|
820
841
|
{
|
|
821
842
|
"type": "text",
|
|
822
|
-
"text": f"Error calling tool {tool_name}: {str(e)}"
|
|
843
|
+
"text": f"Error calling tool {tool_name}: {str(e)}",
|
|
823
844
|
}
|
|
824
845
|
],
|
|
825
|
-
"isError": True
|
|
846
|
+
"isError": True,
|
|
826
847
|
}
|
|
827
848
|
|
|
828
849
|
async def run(self) -> None:
|
|
@@ -832,7 +853,9 @@ class MCPTicketServer:
|
|
|
832
853
|
try:
|
|
833
854
|
reader = asyncio.StreamReader()
|
|
834
855
|
protocol = asyncio.StreamReaderProtocol(reader)
|
|
835
|
-
await asyncio.get_event_loop().connect_read_pipe(
|
|
856
|
+
await asyncio.get_event_loop().connect_read_pipe(
|
|
857
|
+
lambda: protocol, sys.stdin
|
|
858
|
+
)
|
|
836
859
|
except Exception as e:
|
|
837
860
|
sys.stderr.write(f"Failed to connect to stdin: {str(e)}\n")
|
|
838
861
|
return
|
|
@@ -858,9 +881,7 @@ class MCPTicketServer:
|
|
|
858
881
|
|
|
859
882
|
except json.JSONDecodeError as e:
|
|
860
883
|
error_response = self._error_response(
|
|
861
|
-
None,
|
|
862
|
-
-32700,
|
|
863
|
-
f"Parse error: {str(e)}"
|
|
884
|
+
None, -32700, f"Parse error: {str(e)}"
|
|
864
885
|
)
|
|
865
886
|
sys.stdout.write(json.dumps(error_response) + "\n")
|
|
866
887
|
sys.stdout.flush()
|
|
@@ -888,23 +909,56 @@ async def main():
|
|
|
888
909
|
|
|
889
910
|
This function is maintained in case it's being called directly,
|
|
890
911
|
but the preferred way is now through the CLI: `mcp-ticketer mcp`
|
|
912
|
+
|
|
913
|
+
SECURITY: This method ONLY reads from the current project directory
|
|
914
|
+
to prevent configuration leakage across projects. It will NEVER read
|
|
915
|
+
from user home directory or system-wide locations.
|
|
891
916
|
"""
|
|
892
917
|
# Load configuration
|
|
893
918
|
import json
|
|
919
|
+
import logging
|
|
894
920
|
from pathlib import Path
|
|
895
921
|
|
|
896
|
-
|
|
922
|
+
logger = logging.getLogger(__name__)
|
|
923
|
+
|
|
924
|
+
# ONLY read from project-local config, never from user home
|
|
925
|
+
config_file = Path.cwd() / ".mcp-ticketer" / "config.json"
|
|
897
926
|
if config_file.exists():
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
927
|
+
# Validate config is within project
|
|
928
|
+
try:
|
|
929
|
+
if not config_file.resolve().is_relative_to(Path.cwd().resolve()):
|
|
930
|
+
logger.error(
|
|
931
|
+
f"Security violation: Config file {config_file} "
|
|
932
|
+
"is not within project directory"
|
|
933
|
+
)
|
|
934
|
+
raise ValueError(
|
|
935
|
+
f"Security violation: Config file {config_file} "
|
|
936
|
+
"is not within project directory"
|
|
937
|
+
)
|
|
938
|
+
except (ValueError, RuntimeError):
|
|
939
|
+
# is_relative_to may raise ValueError in some cases
|
|
940
|
+
pass
|
|
941
|
+
|
|
942
|
+
try:
|
|
943
|
+
with open(config_file) as f:
|
|
944
|
+
config = json.load(f)
|
|
945
|
+
adapter_type = config.get("default_adapter", "aitrackdown")
|
|
946
|
+
# Get adapter-specific config
|
|
947
|
+
adapters_config = config.get("adapters", {})
|
|
948
|
+
adapter_config = adapters_config.get(adapter_type, {})
|
|
949
|
+
# Fallback to legacy config format
|
|
950
|
+
if not adapter_config and "config" in config:
|
|
951
|
+
adapter_config = config["config"]
|
|
952
|
+
logger.info(
|
|
953
|
+
f"Loaded MCP configuration from project-local: {config_file}"
|
|
954
|
+
)
|
|
955
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
956
|
+
logger.warning(f"Could not load project config: {e}, using defaults")
|
|
957
|
+
adapter_type = "aitrackdown"
|
|
958
|
+
adapter_config = {"base_path": ".aitrackdown"}
|
|
907
959
|
else:
|
|
960
|
+
# Default to aitrackdown with local base path
|
|
961
|
+
logger.info("No project-local config found, defaulting to aitrackdown adapter")
|
|
908
962
|
adapter_type = "aitrackdown"
|
|
909
963
|
adapter_config = {"base_path": ".aitrackdown"}
|
|
910
964
|
|
|
@@ -914,4 +968,4 @@ async def main():
|
|
|
914
968
|
|
|
915
969
|
|
|
916
970
|
if __name__ == "__main__":
|
|
917
|
-
asyncio.run(main())
|
|
971
|
+
asyncio.run(main())
|