mcp-ticketer 0.1.20__py3-none-any.whl → 0.1.22__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 +54 -38
- mcp_ticketer/adapters/github.py +175 -109
- mcp_ticketer/adapters/hybrid.py +90 -45
- mcp_ticketer/adapters/jira.py +139 -130
- mcp_ticketer/adapters/linear.py +374 -225
- mcp_ticketer/cache/__init__.py +1 -1
- mcp_ticketer/cache/memory.py +14 -15
- 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 +250 -293
- mcp_ticketer/cli/mcp_configure.py +39 -15
- mcp_ticketer/cli/migrate_config.py +10 -12
- mcp_ticketer/cli/queue_commands.py +21 -58
- mcp_ticketer/cli/utils.py +115 -60
- mcp_ticketer/core/__init__.py +2 -2
- mcp_ticketer/core/adapter.py +36 -30
- mcp_ticketer/core/config.py +113 -77
- mcp_ticketer/core/env_discovery.py +51 -19
- mcp_ticketer/core/http_client.py +46 -29
- mcp_ticketer/core/mappers.py +79 -35
- mcp_ticketer/core/models.py +29 -15
- mcp_ticketer/core/project_config.py +131 -66
- mcp_ticketer/core/registry.py +12 -12
- mcp_ticketer/mcp/__init__.py +1 -1
- mcp_ticketer/mcp/server.py +183 -129
- mcp_ticketer/queue/__init__.py +2 -2
- mcp_ticketer/queue/__main__.py +1 -1
- mcp_ticketer/queue/manager.py +29 -25
- mcp_ticketer/queue/queue.py +144 -82
- mcp_ticketer/queue/run_worker.py +2 -3
- mcp_ticketer/queue/worker.py +48 -33
- {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/METADATA +1 -1
- mcp_ticketer-0.1.22.dist-info/RECORD +42 -0
- mcp_ticketer-0.1.20.dist-info/RECORD +0 -42
- {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.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, Dict, List, 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,16 +33,18 @@ 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
|
|
|
@@ -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,29 +95,16 @@ 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
|
-
request_id: Any,
|
|
116
|
-
code: int,
|
|
117
|
-
message: str
|
|
107
|
+
self, request_id: Any, code: int, message: str
|
|
118
108
|
) -> Dict[str, Any]:
|
|
119
109
|
"""Create error response.
|
|
120
110
|
|
|
@@ -125,14 +115,12 @@ 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
126
|
async def _handle_create(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -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,7 +210,7 @@ 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
|
|
@@ -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,7 +289,7 @@ 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
|
|
@@ -308,7 +302,7 @@ class MCPTicketServer:
|
|
|
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,7 +312,7 @@ 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
318
|
async def _handle_list(self, params: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
@@ -326,7 +320,7 @@ class MCPTicketServer:
|
|
|
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
|
|
|
@@ -343,10 +337,10 @@ class MCPTicketServer:
|
|
|
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,7 +399,7 @@ 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
|
|
@@ -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,7 +437,7 @@ 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
|
|
|
@@ -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:
|
|
@@ -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
|
|
@@ -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(
|
|
@@ -627,18 +625,12 @@ 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
636
|
async def _handle_tools_list(self) -> Dict[str, Any]:
|
|
@@ -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,10 +770,13 @@ 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
|
}
|
|
@@ -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())
|