mcp-ticketer 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__init__.py +27 -0
- mcp_ticketer/__version__.py +40 -0
- mcp_ticketer/adapters/__init__.py +8 -0
- mcp_ticketer/adapters/aitrackdown.py +396 -0
- mcp_ticketer/adapters/github.py +974 -0
- mcp_ticketer/adapters/jira.py +831 -0
- mcp_ticketer/adapters/linear.py +1355 -0
- mcp_ticketer/cache/__init__.py +5 -0
- mcp_ticketer/cache/memory.py +193 -0
- mcp_ticketer/cli/__init__.py +5 -0
- mcp_ticketer/cli/main.py +812 -0
- mcp_ticketer/cli/queue_commands.py +285 -0
- mcp_ticketer/cli/utils.py +523 -0
- mcp_ticketer/core/__init__.py +15 -0
- mcp_ticketer/core/adapter.py +211 -0
- mcp_ticketer/core/config.py +403 -0
- mcp_ticketer/core/http_client.py +430 -0
- mcp_ticketer/core/mappers.py +492 -0
- mcp_ticketer/core/models.py +111 -0
- mcp_ticketer/core/registry.py +128 -0
- mcp_ticketer/mcp/__init__.py +5 -0
- mcp_ticketer/mcp/server.py +459 -0
- mcp_ticketer/py.typed +0 -0
- mcp_ticketer/queue/__init__.py +7 -0
- mcp_ticketer/queue/__main__.py +6 -0
- mcp_ticketer/queue/manager.py +261 -0
- mcp_ticketer/queue/queue.py +357 -0
- mcp_ticketer/queue/run_worker.py +38 -0
- mcp_ticketer/queue/worker.py +425 -0
- mcp_ticketer-0.1.1.dist-info/METADATA +362 -0
- mcp_ticketer-0.1.1.dist-info/RECORD +35 -0
- mcp_ticketer-0.1.1.dist-info/WHEEL +5 -0
- mcp_ticketer-0.1.1.dist-info/entry_points.txt +3 -0
- mcp_ticketer-0.1.1.dist-info/licenses/LICENSE +21 -0
- mcp_ticketer-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Adapter registry for dynamic adapter management."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Type, Any, Optional
|
|
4
|
+
from .adapter import BaseAdapter
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AdapterRegistry:
|
|
8
|
+
"""Registry for managing ticket system adapters."""
|
|
9
|
+
|
|
10
|
+
_adapters: Dict[str, Type[BaseAdapter]] = {}
|
|
11
|
+
_instances: Dict[str, BaseAdapter] = {}
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def register(cls, name: str, adapter_class: Type[BaseAdapter]) -> None:
|
|
15
|
+
"""Register an adapter class.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
name: Unique name for the adapter
|
|
19
|
+
adapter_class: Adapter class to register
|
|
20
|
+
"""
|
|
21
|
+
if not issubclass(adapter_class, BaseAdapter):
|
|
22
|
+
raise TypeError(f"{adapter_class} must be a subclass of BaseAdapter")
|
|
23
|
+
cls._adapters[name] = adapter_class
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def unregister(cls, name: str) -> None:
|
|
27
|
+
"""Unregister an adapter.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
name: Name of adapter to unregister
|
|
31
|
+
"""
|
|
32
|
+
cls._adapters.pop(name, None)
|
|
33
|
+
cls._instances.pop(name, None)
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def get_adapter(
|
|
37
|
+
cls,
|
|
38
|
+
name: str,
|
|
39
|
+
config: Optional[Dict[str, Any]] = None,
|
|
40
|
+
force_new: bool = False
|
|
41
|
+
) -> BaseAdapter:
|
|
42
|
+
"""Get or create an adapter instance.
|
|
43
|
+
|
|
44
|
+
Uses factory pattern for adapter instantiation with caching.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
name: Name of the registered adapter
|
|
48
|
+
config: Configuration for the adapter
|
|
49
|
+
force_new: Force creation of new instance
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Adapter instance
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
ValueError: If adapter not registered
|
|
56
|
+
"""
|
|
57
|
+
if name not in cls._adapters:
|
|
58
|
+
available = ", ".join(cls._adapters.keys())
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"Adapter '{name}' not registered. "
|
|
61
|
+
f"Available adapters: {available}"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Return cached instance if exists and not forcing new
|
|
65
|
+
if name in cls._instances and not force_new:
|
|
66
|
+
return cls._instances[name]
|
|
67
|
+
|
|
68
|
+
# Create new instance
|
|
69
|
+
adapter_class = cls._adapters[name]
|
|
70
|
+
config = config or {}
|
|
71
|
+
instance = adapter_class(config)
|
|
72
|
+
|
|
73
|
+
# Cache the instance
|
|
74
|
+
cls._instances[name] = instance
|
|
75
|
+
return instance
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def list_adapters(cls) -> Dict[str, Type[BaseAdapter]]:
|
|
79
|
+
"""List all registered adapters.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Dictionary of adapter names to classes
|
|
83
|
+
"""
|
|
84
|
+
return cls._adapters.copy()
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def is_registered(cls, name: str) -> bool:
|
|
88
|
+
"""Check if an adapter is registered.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
name: Adapter name to check
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
True if registered
|
|
95
|
+
"""
|
|
96
|
+
return name in cls._adapters
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
async def close_all(cls) -> None:
|
|
100
|
+
"""Close all adapter instances and clear cache."""
|
|
101
|
+
for instance in cls._instances.values():
|
|
102
|
+
await instance.close()
|
|
103
|
+
cls._instances.clear()
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def clear_registry(cls) -> None:
|
|
107
|
+
"""Clear all registrations and instances.
|
|
108
|
+
|
|
109
|
+
Useful for testing or reinitialization.
|
|
110
|
+
"""
|
|
111
|
+
cls._adapters.clear()
|
|
112
|
+
cls._instances.clear()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def adapter_factory(
|
|
116
|
+
adapter_type: str,
|
|
117
|
+
config: Dict[str, Any]
|
|
118
|
+
) -> BaseAdapter:
|
|
119
|
+
"""Factory function for creating adapters.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
adapter_type: Type of adapter to create
|
|
123
|
+
config: Configuration for the adapter
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Configured adapter instance
|
|
127
|
+
"""
|
|
128
|
+
return AdapterRegistry.get_adapter(adapter_type, config)
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""MCP JSON-RPC server for ticket management."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from ..core import Task, TicketState, Priority, AdapterRegistry
|
|
9
|
+
from ..core.models import SearchQuery, Comment
|
|
10
|
+
from ..adapters import AITrackdownAdapter
|
|
11
|
+
from ..queue import Queue, QueueStatus, WorkerManager
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MCPTicketServer:
|
|
15
|
+
"""MCP server for ticket operations over stdio."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, adapter_type: str = "aitrackdown", config: Optional[Dict[str, Any]] = None):
|
|
18
|
+
"""Initialize MCP server.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
adapter_type: Type of adapter to use
|
|
22
|
+
config: Adapter configuration
|
|
23
|
+
"""
|
|
24
|
+
self.adapter = AdapterRegistry.get_adapter(
|
|
25
|
+
adapter_type,
|
|
26
|
+
config or {"base_path": ".aitrackdown"}
|
|
27
|
+
)
|
|
28
|
+
self.running = False
|
|
29
|
+
|
|
30
|
+
async def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
|
|
31
|
+
"""Handle JSON-RPC request.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
request: JSON-RPC request
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
JSON-RPC response
|
|
38
|
+
"""
|
|
39
|
+
method = request.get("method")
|
|
40
|
+
params = request.get("params", {})
|
|
41
|
+
request_id = request.get("id")
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
# Route to appropriate handler
|
|
45
|
+
if method == "ticket/create":
|
|
46
|
+
result = await self._handle_create(params)
|
|
47
|
+
elif method == "ticket/read":
|
|
48
|
+
result = await self._handle_read(params)
|
|
49
|
+
elif method == "ticket/update":
|
|
50
|
+
result = await self._handle_update(params)
|
|
51
|
+
elif method == "ticket/delete":
|
|
52
|
+
result = await self._handle_delete(params)
|
|
53
|
+
elif method == "ticket/list":
|
|
54
|
+
result = await self._handle_list(params)
|
|
55
|
+
elif method == "ticket/search":
|
|
56
|
+
result = await self._handle_search(params)
|
|
57
|
+
elif method == "ticket/transition":
|
|
58
|
+
result = await self._handle_transition(params)
|
|
59
|
+
elif method == "ticket/comment":
|
|
60
|
+
result = await self._handle_comment(params)
|
|
61
|
+
elif method == "ticket/status":
|
|
62
|
+
result = await self._handle_queue_status(params)
|
|
63
|
+
elif method == "tools/list":
|
|
64
|
+
result = await self._handle_tools_list()
|
|
65
|
+
else:
|
|
66
|
+
return self._error_response(
|
|
67
|
+
request_id,
|
|
68
|
+
-32601,
|
|
69
|
+
f"Method not found: {method}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
"jsonrpc": "2.0",
|
|
74
|
+
"result": result,
|
|
75
|
+
"id": request_id
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
except Exception as e:
|
|
79
|
+
return self._error_response(
|
|
80
|
+
request_id,
|
|
81
|
+
-32603,
|
|
82
|
+
f"Internal error: {str(e)}"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def _error_response(
|
|
86
|
+
self,
|
|
87
|
+
request_id: Any,
|
|
88
|
+
code: int,
|
|
89
|
+
message: str
|
|
90
|
+
) -> Dict[str, Any]:
|
|
91
|
+
"""Create error response.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
request_id: Request ID
|
|
95
|
+
code: Error code
|
|
96
|
+
message: Error message
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Error response
|
|
100
|
+
"""
|
|
101
|
+
return {
|
|
102
|
+
"jsonrpc": "2.0",
|
|
103
|
+
"error": {
|
|
104
|
+
"code": code,
|
|
105
|
+
"message": message
|
|
106
|
+
},
|
|
107
|
+
"id": request_id
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async def _handle_create(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
111
|
+
"""Handle ticket creation."""
|
|
112
|
+
# Queue the operation instead of direct execution
|
|
113
|
+
queue = Queue()
|
|
114
|
+
task_data = {
|
|
115
|
+
"title": params["title"],
|
|
116
|
+
"description": params.get("description"),
|
|
117
|
+
"priority": params.get("priority", "medium"),
|
|
118
|
+
"tags": params.get("tags", []),
|
|
119
|
+
"assignee": params.get("assignee"),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
queue_id = queue.add(
|
|
123
|
+
ticket_data=task_data,
|
|
124
|
+
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
125
|
+
operation="create"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Start worker if needed
|
|
129
|
+
manager = WorkerManager()
|
|
130
|
+
manager.start_if_needed()
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
"queue_id": queue_id,
|
|
134
|
+
"status": "queued",
|
|
135
|
+
"message": f"Ticket creation queued with ID: {queue_id}"
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async def _handle_read(self, params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
139
|
+
"""Handle ticket read."""
|
|
140
|
+
ticket = await self.adapter.read(params["ticket_id"])
|
|
141
|
+
return ticket.model_dump() if ticket else None
|
|
142
|
+
|
|
143
|
+
async def _handle_update(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
144
|
+
"""Handle ticket update."""
|
|
145
|
+
# Queue the operation
|
|
146
|
+
queue = Queue()
|
|
147
|
+
updates = params.get("updates", {})
|
|
148
|
+
updates["ticket_id"] = params["ticket_id"]
|
|
149
|
+
|
|
150
|
+
queue_id = queue.add(
|
|
151
|
+
ticket_data=updates,
|
|
152
|
+
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
153
|
+
operation="update"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Start worker if needed
|
|
157
|
+
manager = WorkerManager()
|
|
158
|
+
manager.start_if_needed()
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
"queue_id": queue_id,
|
|
162
|
+
"status": "queued",
|
|
163
|
+
"message": f"Ticket update queued with ID: {queue_id}"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async def _handle_delete(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
167
|
+
"""Handle ticket deletion."""
|
|
168
|
+
# Queue the operation
|
|
169
|
+
queue = Queue()
|
|
170
|
+
queue_id = queue.add(
|
|
171
|
+
ticket_data={"ticket_id": params["ticket_id"]},
|
|
172
|
+
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
173
|
+
operation="delete"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Start worker if needed
|
|
177
|
+
manager = WorkerManager()
|
|
178
|
+
manager.start_if_needed()
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
"queue_id": queue_id,
|
|
182
|
+
"status": "queued",
|
|
183
|
+
"message": f"Ticket deletion queued with ID: {queue_id}"
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async def _handle_list(self, params: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
187
|
+
"""Handle ticket listing."""
|
|
188
|
+
tickets = await self.adapter.list(
|
|
189
|
+
limit=params.get("limit", 10),
|
|
190
|
+
offset=params.get("offset", 0),
|
|
191
|
+
filters=params.get("filters")
|
|
192
|
+
)
|
|
193
|
+
return [ticket.model_dump() for ticket in tickets]
|
|
194
|
+
|
|
195
|
+
async def _handle_search(self, params: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
196
|
+
"""Handle ticket search."""
|
|
197
|
+
query = SearchQuery(**params)
|
|
198
|
+
tickets = await self.adapter.search(query)
|
|
199
|
+
return [ticket.model_dump() for ticket in tickets]
|
|
200
|
+
|
|
201
|
+
async def _handle_transition(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
202
|
+
"""Handle state transition."""
|
|
203
|
+
# Queue the operation
|
|
204
|
+
queue = Queue()
|
|
205
|
+
queue_id = queue.add(
|
|
206
|
+
ticket_data={
|
|
207
|
+
"ticket_id": params["ticket_id"],
|
|
208
|
+
"state": params["target_state"]
|
|
209
|
+
},
|
|
210
|
+
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
211
|
+
operation="transition"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Start worker if needed
|
|
215
|
+
manager = WorkerManager()
|
|
216
|
+
manager.start_if_needed()
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
"queue_id": queue_id,
|
|
220
|
+
"status": "queued",
|
|
221
|
+
"message": f"State transition queued with ID: {queue_id}"
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async def _handle_comment(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
225
|
+
"""Handle comment operations."""
|
|
226
|
+
operation = params.get("operation", "add")
|
|
227
|
+
|
|
228
|
+
if operation == "add":
|
|
229
|
+
# Queue the comment addition
|
|
230
|
+
queue = Queue()
|
|
231
|
+
queue_id = queue.add(
|
|
232
|
+
ticket_data={
|
|
233
|
+
"ticket_id": params["ticket_id"],
|
|
234
|
+
"content": params["content"],
|
|
235
|
+
"author": params.get("author")
|
|
236
|
+
},
|
|
237
|
+
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
238
|
+
operation="comment"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Start worker if needed
|
|
242
|
+
manager = WorkerManager()
|
|
243
|
+
manager.start_if_needed()
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
"queue_id": queue_id,
|
|
247
|
+
"status": "queued",
|
|
248
|
+
"message": f"Comment addition queued with ID: {queue_id}"
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
elif operation == "list":
|
|
252
|
+
# Comments list is read-only, execute directly
|
|
253
|
+
comments = await self.adapter.get_comments(
|
|
254
|
+
params["ticket_id"],
|
|
255
|
+
limit=params.get("limit", 10),
|
|
256
|
+
offset=params.get("offset", 0)
|
|
257
|
+
)
|
|
258
|
+
return [comment.model_dump() for comment in comments]
|
|
259
|
+
|
|
260
|
+
else:
|
|
261
|
+
raise ValueError(f"Unknown comment operation: {operation}")
|
|
262
|
+
|
|
263
|
+
async def _handle_queue_status(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
264
|
+
"""Check status of queued operation."""
|
|
265
|
+
queue_id = params.get("queue_id")
|
|
266
|
+
if not queue_id:
|
|
267
|
+
raise ValueError("queue_id is required")
|
|
268
|
+
|
|
269
|
+
queue = Queue()
|
|
270
|
+
item = queue.get_item(queue_id)
|
|
271
|
+
|
|
272
|
+
if not item:
|
|
273
|
+
return {
|
|
274
|
+
"error": f"Queue item not found: {queue_id}"
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
response = {
|
|
278
|
+
"queue_id": item.id,
|
|
279
|
+
"status": item.status.value,
|
|
280
|
+
"operation": item.operation,
|
|
281
|
+
"created_at": item.created_at.isoformat(),
|
|
282
|
+
"retry_count": item.retry_count
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if item.processed_at:
|
|
286
|
+
response["processed_at"] = item.processed_at.isoformat()
|
|
287
|
+
|
|
288
|
+
if item.error_message:
|
|
289
|
+
response["error"] = item.error_message
|
|
290
|
+
|
|
291
|
+
if item.result:
|
|
292
|
+
response["result"] = item.result
|
|
293
|
+
|
|
294
|
+
return response
|
|
295
|
+
|
|
296
|
+
async def _handle_tools_list(self) -> Dict[str, Any]:
|
|
297
|
+
"""List available MCP tools."""
|
|
298
|
+
return {
|
|
299
|
+
"tools": [
|
|
300
|
+
{
|
|
301
|
+
"name": "ticket_create",
|
|
302
|
+
"description": "Create a new ticket",
|
|
303
|
+
"parameters": {
|
|
304
|
+
"type": "object",
|
|
305
|
+
"properties": {
|
|
306
|
+
"title": {"type": "string", "description": "Ticket title"},
|
|
307
|
+
"description": {"type": "string", "description": "Description"},
|
|
308
|
+
"priority": {"type": "string", "enum": ["low", "medium", "high", "critical"]},
|
|
309
|
+
"tags": {"type": "array", "items": {"type": "string"}},
|
|
310
|
+
"assignee": {"type": "string"},
|
|
311
|
+
},
|
|
312
|
+
"required": ["title"]
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"name": "ticket_list",
|
|
317
|
+
"description": "List tickets",
|
|
318
|
+
"parameters": {
|
|
319
|
+
"type": "object",
|
|
320
|
+
"properties": {
|
|
321
|
+
"limit": {"type": "integer", "default": 10},
|
|
322
|
+
"state": {"type": "string"},
|
|
323
|
+
"priority": {"type": "string"},
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
"name": "ticket_update",
|
|
329
|
+
"description": "Update a ticket",
|
|
330
|
+
"parameters": {
|
|
331
|
+
"type": "object",
|
|
332
|
+
"properties": {
|
|
333
|
+
"ticket_id": {"type": "string", "description": "Ticket ID"},
|
|
334
|
+
"updates": {"type": "object", "description": "Fields to update"},
|
|
335
|
+
},
|
|
336
|
+
"required": ["ticket_id", "updates"]
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
"name": "ticket_transition",
|
|
341
|
+
"description": "Change ticket state",
|
|
342
|
+
"parameters": {
|
|
343
|
+
"type": "object",
|
|
344
|
+
"properties": {
|
|
345
|
+
"ticket_id": {"type": "string"},
|
|
346
|
+
"target_state": {"type": "string"},
|
|
347
|
+
},
|
|
348
|
+
"required": ["ticket_id", "target_state"]
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
"name": "ticket_search",
|
|
353
|
+
"description": "Search tickets",
|
|
354
|
+
"parameters": {
|
|
355
|
+
"type": "object",
|
|
356
|
+
"properties": {
|
|
357
|
+
"query": {"type": "string"},
|
|
358
|
+
"state": {"type": "string"},
|
|
359
|
+
"priority": {"type": "string"},
|
|
360
|
+
"limit": {"type": "integer", "default": 10},
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
"name": "ticket_status",
|
|
366
|
+
"description": "Check status of queued ticket operation",
|
|
367
|
+
"parameters": {
|
|
368
|
+
"type": "object",
|
|
369
|
+
"properties": {
|
|
370
|
+
"queue_id": {"type": "string", "description": "Queue ID returned from create/update/delete operations"},
|
|
371
|
+
},
|
|
372
|
+
"required": ["queue_id"]
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
]
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async def run(self) -> None:
|
|
379
|
+
"""Run the MCP server, reading from stdin and writing to stdout."""
|
|
380
|
+
self.running = True
|
|
381
|
+
reader = asyncio.StreamReader()
|
|
382
|
+
protocol = asyncio.StreamReaderProtocol(reader)
|
|
383
|
+
await asyncio.get_event_loop().connect_read_pipe(lambda: protocol, sys.stdin)
|
|
384
|
+
|
|
385
|
+
# Send initialization
|
|
386
|
+
init_message = {
|
|
387
|
+
"jsonrpc": "2.0",
|
|
388
|
+
"method": "initialized",
|
|
389
|
+
"params": {
|
|
390
|
+
"name": "mcp-ticketer",
|
|
391
|
+
"version": "0.1.0",
|
|
392
|
+
"capabilities": ["tickets", "comments", "search"]
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
sys.stdout.write(json.dumps(init_message) + "\n")
|
|
396
|
+
sys.stdout.flush()
|
|
397
|
+
|
|
398
|
+
# Main message loop
|
|
399
|
+
while self.running:
|
|
400
|
+
try:
|
|
401
|
+
line = await reader.readline()
|
|
402
|
+
if not line:
|
|
403
|
+
break
|
|
404
|
+
|
|
405
|
+
# Parse JSON-RPC request
|
|
406
|
+
request = json.loads(line.decode())
|
|
407
|
+
|
|
408
|
+
# Handle request
|
|
409
|
+
response = await self.handle_request(request)
|
|
410
|
+
|
|
411
|
+
# Send response
|
|
412
|
+
sys.stdout.write(json.dumps(response) + "\n")
|
|
413
|
+
sys.stdout.flush()
|
|
414
|
+
|
|
415
|
+
except json.JSONDecodeError as e:
|
|
416
|
+
error_response = self._error_response(
|
|
417
|
+
None,
|
|
418
|
+
-32700,
|
|
419
|
+
f"Parse error: {str(e)}"
|
|
420
|
+
)
|
|
421
|
+
sys.stdout.write(json.dumps(error_response) + "\n")
|
|
422
|
+
sys.stdout.flush()
|
|
423
|
+
|
|
424
|
+
except KeyboardInterrupt:
|
|
425
|
+
break
|
|
426
|
+
|
|
427
|
+
except Exception as e:
|
|
428
|
+
# Log error but continue running
|
|
429
|
+
sys.stderr.write(f"Error: {str(e)}\n")
|
|
430
|
+
|
|
431
|
+
async def stop(self) -> None:
|
|
432
|
+
"""Stop the server."""
|
|
433
|
+
self.running = False
|
|
434
|
+
await self.adapter.close()
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
async def main():
|
|
438
|
+
"""Main entry point for MCP server."""
|
|
439
|
+
# Load configuration
|
|
440
|
+
import json
|
|
441
|
+
from pathlib import Path
|
|
442
|
+
|
|
443
|
+
config_file = Path.home() / ".mcp-ticketer" / "config.json"
|
|
444
|
+
if config_file.exists():
|
|
445
|
+
with open(config_file, "r") as f:
|
|
446
|
+
config = json.load(f)
|
|
447
|
+
adapter_type = config.get("adapter", "aitrackdown")
|
|
448
|
+
adapter_config = config.get("config", {})
|
|
449
|
+
else:
|
|
450
|
+
adapter_type = "aitrackdown"
|
|
451
|
+
adapter_config = {"base_path": ".aitrackdown"}
|
|
452
|
+
|
|
453
|
+
# Create and run server
|
|
454
|
+
server = MCPTicketServer(adapter_type, adapter_config)
|
|
455
|
+
await server.run()
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
if __name__ == "__main__":
|
|
459
|
+
asyncio.run(main())
|
mcp_ticketer/py.typed
ADDED
|
File without changes
|