mcp-ticketer 0.3.3__py3-none-any.whl → 0.3.5__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/__version__.py +1 -1
- mcp_ticketer/adapters/linear/adapter.py +103 -1
- mcp_ticketer/adapters/linear/mappers.py +2 -3
- mcp_ticketer/adapters/linear/queries.py +1 -1
- mcp_ticketer/cli/main.py +15 -0
- mcp_ticketer/core/models.py +30 -2
- {mcp_ticketer-0.3.3.dist-info → mcp_ticketer-0.3.5.dist-info}/METADATA +1 -1
- {mcp_ticketer-0.3.3.dist-info → mcp_ticketer-0.3.5.dist-info}/RECORD +12 -12
- {mcp_ticketer-0.3.3.dist-info → mcp_ticketer-0.3.5.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.3.dist-info → mcp_ticketer-0.3.5.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.3.dist-info → mcp_ticketer-0.3.5.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.3.3.dist-info → mcp_ticketer-0.3.5.dist-info}/top_level.txt +0 -0
mcp_ticketer/__version__.py
CHANGED
|
@@ -137,8 +137,9 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
137
137
|
# Load team data and workflow states concurrently
|
|
138
138
|
team_id = await self._ensure_team_id()
|
|
139
139
|
|
|
140
|
-
# Load workflow states for the team
|
|
140
|
+
# Load workflow states and labels for the team
|
|
141
141
|
await self._load_workflow_states(team_id)
|
|
142
|
+
await self._load_team_labels(team_id)
|
|
142
143
|
|
|
143
144
|
self._initialized = True
|
|
144
145
|
|
|
@@ -216,6 +217,85 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
216
217
|
except Exception as e:
|
|
217
218
|
raise ValueError(f"Failed to load workflow states: {e}")
|
|
218
219
|
|
|
220
|
+
async def _load_team_labels(self, team_id: str) -> None:
|
|
221
|
+
"""Load and cache labels for the team.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
team_id: Linear team ID
|
|
225
|
+
|
|
226
|
+
"""
|
|
227
|
+
query = """
|
|
228
|
+
query GetTeamLabels($teamId: String!) {
|
|
229
|
+
team(id: $teamId) {
|
|
230
|
+
labels {
|
|
231
|
+
nodes {
|
|
232
|
+
id
|
|
233
|
+
name
|
|
234
|
+
color
|
|
235
|
+
description
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
result = await self.client.execute_query(query, {"teamId": team_id})
|
|
244
|
+
self._labels_cache = result["team"]["labels"]["nodes"]
|
|
245
|
+
except Exception:
|
|
246
|
+
# Log error but don't fail - labels are optional
|
|
247
|
+
self._labels_cache = []
|
|
248
|
+
|
|
249
|
+
async def _resolve_label_ids(self, label_names: list[str]) -> list[str]:
|
|
250
|
+
"""Resolve label names to Linear label IDs.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
label_names: List of label names
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
List of Linear label IDs that exist
|
|
257
|
+
|
|
258
|
+
"""
|
|
259
|
+
import logging
|
|
260
|
+
|
|
261
|
+
logger = logging.getLogger(__name__)
|
|
262
|
+
|
|
263
|
+
if not self._labels_cache:
|
|
264
|
+
team_id = await self._ensure_team_id()
|
|
265
|
+
await self._load_team_labels(team_id)
|
|
266
|
+
|
|
267
|
+
if not self._labels_cache:
|
|
268
|
+
logger.warning("No labels found in team cache")
|
|
269
|
+
return []
|
|
270
|
+
|
|
271
|
+
# Create name -> ID mapping (case-insensitive)
|
|
272
|
+
label_map = {label["name"].lower(): label["id"] for label in self._labels_cache}
|
|
273
|
+
|
|
274
|
+
logger.debug(f"Available labels in team: {list(label_map.keys())}")
|
|
275
|
+
|
|
276
|
+
# Resolve label names to IDs
|
|
277
|
+
label_ids = []
|
|
278
|
+
unmatched_labels = []
|
|
279
|
+
|
|
280
|
+
for name in label_names:
|
|
281
|
+
label_id = label_map.get(name.lower())
|
|
282
|
+
if label_id:
|
|
283
|
+
label_ids.append(label_id)
|
|
284
|
+
logger.debug(f"Resolved label '{name}' to ID: {label_id}")
|
|
285
|
+
else:
|
|
286
|
+
unmatched_labels.append(name)
|
|
287
|
+
logger.warning(
|
|
288
|
+
f"Label '{name}' not found in team. Available labels: {list(label_map.keys())}"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
if unmatched_labels:
|
|
292
|
+
logger.warning(
|
|
293
|
+
f"Could not resolve labels: {unmatched_labels}. "
|
|
294
|
+
f"Create them in Linear first or check spelling."
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
return label_ids
|
|
298
|
+
|
|
219
299
|
def _get_state_mapping(self) -> dict[TicketState, str]:
|
|
220
300
|
"""Get mapping from universal states to Linear workflow state IDs.
|
|
221
301
|
|
|
@@ -311,12 +391,28 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
311
391
|
# Build issue input using mapper
|
|
312
392
|
issue_input = build_linear_issue_input(task, team_id)
|
|
313
393
|
|
|
394
|
+
# Set default state if not provided
|
|
395
|
+
# Map OPEN to "unstarted" state (typically "To-Do" in Linear)
|
|
396
|
+
if task.state == TicketState.OPEN and self._workflow_states:
|
|
397
|
+
state_mapping = self._get_state_mapping()
|
|
398
|
+
if TicketState.OPEN in state_mapping:
|
|
399
|
+
issue_input["stateId"] = state_mapping[TicketState.OPEN]
|
|
400
|
+
|
|
314
401
|
# Resolve assignee to user ID if provided
|
|
315
402
|
if task.assignee:
|
|
316
403
|
user_id = await self._get_user_id(task.assignee)
|
|
317
404
|
if user_id:
|
|
318
405
|
issue_input["assigneeId"] = user_id
|
|
319
406
|
|
|
407
|
+
# Resolve label names to IDs if provided
|
|
408
|
+
if task.tags:
|
|
409
|
+
label_ids = await self._resolve_label_ids(task.tags)
|
|
410
|
+
if label_ids:
|
|
411
|
+
issue_input["labelIds"] = label_ids
|
|
412
|
+
else:
|
|
413
|
+
# Remove labelIds if no labels resolved
|
|
414
|
+
issue_input.pop("labelIds", None)
|
|
415
|
+
|
|
320
416
|
try:
|
|
321
417
|
result = await self.client.execute_mutation(
|
|
322
418
|
CREATE_ISSUE_MUTATION, {"input": issue_input}
|
|
@@ -489,6 +585,12 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
489
585
|
if user_id:
|
|
490
586
|
update_input["assigneeId"] = user_id
|
|
491
587
|
|
|
588
|
+
# Resolve label names to IDs if provided
|
|
589
|
+
if "tags" in updates and updates["tags"]:
|
|
590
|
+
label_ids = await self._resolve_label_ids(updates["tags"])
|
|
591
|
+
if label_ids:
|
|
592
|
+
update_input["labelIds"] = label_ids
|
|
593
|
+
|
|
492
594
|
# Execute update
|
|
493
595
|
result = await self.client.execute_mutation(
|
|
494
596
|
UPDATE_ISSUE_MUTATION, {"id": linear_id, "input": update_input}
|
|
@@ -242,9 +242,8 @@ def build_linear_issue_input(task: Task, team_id: str) -> dict[str, Any]:
|
|
|
242
242
|
|
|
243
243
|
# Add labels (tags) if provided
|
|
244
244
|
if task.tags:
|
|
245
|
-
# Note:
|
|
246
|
-
|
|
247
|
-
pass
|
|
245
|
+
# Note: This returns label names, will be resolved to IDs by adapter
|
|
246
|
+
issue_input["labelIds"] = task.tags # Temporary - adapter will resolve
|
|
248
247
|
|
|
249
248
|
# Add Linear-specific metadata
|
|
250
249
|
if task.metadata and "linear" in task.metadata:
|
|
@@ -225,7 +225,7 @@ ISSUE_LIST_FRAGMENTS = (
|
|
|
225
225
|
# Query definitions
|
|
226
226
|
|
|
227
227
|
WORKFLOW_STATES_QUERY = """
|
|
228
|
-
query WorkflowStates($teamId:
|
|
228
|
+
query WorkflowStates($teamId: String!) {
|
|
229
229
|
workflowStates(filter: { team: { id: { eq: $teamId } } }) {
|
|
230
230
|
nodes {
|
|
231
231
|
id
|
mcp_ticketer/cli/main.py
CHANGED
|
@@ -1266,6 +1266,16 @@ def create(
|
|
|
1266
1266
|
assignee: Optional[str] = typer.Option(
|
|
1267
1267
|
None, "--assignee", "-a", help="Assignee username"
|
|
1268
1268
|
),
|
|
1269
|
+
project: Optional[str] = typer.Option(
|
|
1270
|
+
None,
|
|
1271
|
+
"--project",
|
|
1272
|
+
help="Parent project/epic ID (synonym for --epic)",
|
|
1273
|
+
),
|
|
1274
|
+
epic: Optional[str] = typer.Option(
|
|
1275
|
+
None,
|
|
1276
|
+
"--epic",
|
|
1277
|
+
help="Parent epic/project ID (synonym for --project)",
|
|
1278
|
+
),
|
|
1269
1279
|
adapter: Optional[AdapterType] = typer.Option(
|
|
1270
1280
|
None, "--adapter", help="Override default adapter"
|
|
1271
1281
|
),
|
|
@@ -1337,6 +1347,9 @@ def create(
|
|
|
1337
1347
|
# Priority 4: Default
|
|
1338
1348
|
adapter_name = "aitrackdown"
|
|
1339
1349
|
|
|
1350
|
+
# Resolve project/epic synonym - prefer whichever is provided
|
|
1351
|
+
parent_epic_id = project or epic
|
|
1352
|
+
|
|
1340
1353
|
# Create task data
|
|
1341
1354
|
# Import Priority for type checking
|
|
1342
1355
|
from ..core.models import Priority as PriorityEnum
|
|
@@ -1347,6 +1360,7 @@ def create(
|
|
|
1347
1360
|
"priority": priority.value if isinstance(priority, PriorityEnum) else priority,
|
|
1348
1361
|
"tags": tags or [],
|
|
1349
1362
|
"assignee": assignee,
|
|
1363
|
+
"parent_epic": parent_epic_id,
|
|
1350
1364
|
}
|
|
1351
1365
|
|
|
1352
1366
|
# WORKAROUND: Use direct operation for Linear adapter to bypass worker subprocess issue
|
|
@@ -1377,6 +1391,7 @@ def create(
|
|
|
1377
1391
|
),
|
|
1378
1392
|
tags=task_data.get("tags", []),
|
|
1379
1393
|
assignee=task_data.get("assignee"),
|
|
1394
|
+
parent_epic=task_data.get("parent_epic"),
|
|
1380
1395
|
)
|
|
1381
1396
|
|
|
1382
1397
|
# Create ticket synchronously
|
mcp_ticketer/core/models.py
CHANGED
|
@@ -260,13 +260,21 @@ class Epic(BaseTicket):
|
|
|
260
260
|
|
|
261
261
|
|
|
262
262
|
class Task(BaseTicket):
|
|
263
|
-
"""Task - individual work item (can be ISSUE or TASK type).
|
|
263
|
+
"""Task - individual work item (can be ISSUE or TASK type).
|
|
264
|
+
|
|
265
|
+
Note: The `project` field is a synonym for `parent_epic` to provide
|
|
266
|
+
flexibility in CLI and API usage. Both fields map to the same underlying
|
|
267
|
+
value (the parent epic/project ID).
|
|
268
|
+
"""
|
|
264
269
|
|
|
265
270
|
ticket_type: TicketType = Field(
|
|
266
271
|
default=TicketType.ISSUE, description="Ticket type in hierarchy"
|
|
267
272
|
)
|
|
268
273
|
parent_issue: Optional[str] = Field(None, description="Parent issue ID (for tasks)")
|
|
269
|
-
parent_epic: Optional[str] = Field(
|
|
274
|
+
parent_epic: Optional[str] = Field(
|
|
275
|
+
None,
|
|
276
|
+
description="Parent epic/project ID (for issues). Synonym: 'project'",
|
|
277
|
+
)
|
|
270
278
|
assignee: Optional[str] = Field(None, description="Assigned user")
|
|
271
279
|
children: list[str] = Field(default_factory=list, description="Child task IDs")
|
|
272
280
|
|
|
@@ -274,6 +282,26 @@ class Task(BaseTicket):
|
|
|
274
282
|
estimated_hours: Optional[float] = Field(None, description="Time estimate")
|
|
275
283
|
actual_hours: Optional[float] = Field(None, description="Actual time spent")
|
|
276
284
|
|
|
285
|
+
@property
|
|
286
|
+
def project(self) -> Optional[str]:
|
|
287
|
+
"""Synonym for parent_epic.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Parent epic/project ID
|
|
291
|
+
|
|
292
|
+
"""
|
|
293
|
+
return self.parent_epic
|
|
294
|
+
|
|
295
|
+
@project.setter
|
|
296
|
+
def project(self, value: Optional[str]) -> None:
|
|
297
|
+
"""Set parent_epic via project synonym.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
value: Parent epic/project ID
|
|
301
|
+
|
|
302
|
+
"""
|
|
303
|
+
self.parent_epic = value
|
|
304
|
+
|
|
277
305
|
def is_epic(self) -> bool:
|
|
278
306
|
"""Check if this is an epic (should use Epic class instead)."""
|
|
279
307
|
return self.ticket_type == TicketType.EPIC
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-ticketer
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Summary: Universal ticket management interface for AI agents with MCP support
|
|
5
5
|
Author-email: MCP Ticketer Team <support@mcp-ticketer.io>
|
|
6
6
|
Maintainer-email: MCP Ticketer Team <support@mcp-ticketer.io>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
mcp_ticketer/__init__.py,sha256=Xx4WaprO5PXhVPbYi1L6tBmwmJMkYS-lMyG4ieN6QP0,717
|
|
2
|
-
mcp_ticketer/__version__.py,sha256=
|
|
2
|
+
mcp_ticketer/__version__.py,sha256=RfLkvzGZCRZI8388D91iayKn1UOAoAbVt1mUwpoGOR8,1117
|
|
3
3
|
mcp_ticketer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
mcp_ticketer/adapters/__init__.py,sha256=B5DFllWn23hkhmrLykNO5uMMSdcFuuPHXyLw_jyFzuE,358
|
|
5
5
|
mcp_ticketer/adapters/aitrackdown.py,sha256=Ecw2SQAGVQs5yMH6m2pj61LxCJsuy-g2bvF8uwTpLUE,22588
|
|
@@ -8,10 +8,10 @@ mcp_ticketer/adapters/hybrid.py,sha256=UADYZLc_UNw0xHPSbgguBNzvUCnuYn12Qi9ea-zdl
|
|
|
8
8
|
mcp_ticketer/adapters/jira.py,sha256=labZFqOy_mmMmizC-RD1EQbu9m4LLtJywwZ956-_x5E,35347
|
|
9
9
|
mcp_ticketer/adapters/linear.py,sha256=trm6ZhmlUl80sj51WAPAox_R2HQZXZ-h1QXJsrFYDCQ,587
|
|
10
10
|
mcp_ticketer/adapters/linear/__init__.py,sha256=6l0ZoR6ZHSRcytLfps2AZuk5R189Pq1GfR5-YDQt8-Q,731
|
|
11
|
-
mcp_ticketer/adapters/linear/adapter.py,sha256=
|
|
11
|
+
mcp_ticketer/adapters/linear/adapter.py,sha256=EvF9FrxMQBVTtO6H3wd9aC8Xv_IeMPt4jM7e2WrsGWU,29610
|
|
12
12
|
mcp_ticketer/adapters/linear/client.py,sha256=0UmWlSEcRiwnSMFYKL89KMrPPL8S8uZ5V6rIY_KFOQU,8803
|
|
13
|
-
mcp_ticketer/adapters/linear/mappers.py,sha256=
|
|
14
|
-
mcp_ticketer/adapters/linear/queries.py,sha256=
|
|
13
|
+
mcp_ticketer/adapters/linear/mappers.py,sha256=GN1X7bOcU-5dhDW3dAtSEGivinhFBc8hoKYot8c5tCo,9631
|
|
14
|
+
mcp_ticketer/adapters/linear/queries.py,sha256=lT6z64eUjX50Mz00Mk7jkFCRKPM9GptHPelWMj2csXc,7200
|
|
15
15
|
mcp_ticketer/adapters/linear/types.py,sha256=ugXtRGLljDw6yoCnEVgdFs0xLR9ErLdnv4ffh9EAUhk,7874
|
|
16
16
|
mcp_ticketer/cache/__init__.py,sha256=Xcd-cKnt-Cx7jBzvfzUUUPaGkmyXFi5XUFWw3Z4b7d4,138
|
|
17
17
|
mcp_ticketer/cache/memory.py,sha256=2yBqGi9i0SanlUhJoOC7nijWjoMa3_ntPe-V-AV-LfU,5042
|
|
@@ -24,7 +24,7 @@ mcp_ticketer/cli/diagnostics.py,sha256=jHF68ydW3RNVGumBnHUjUmq6YOjQD2UDkx0O7M__x
|
|
|
24
24
|
mcp_ticketer/cli/discover.py,sha256=AF_qlQc1Oo0UkWayoF5pmRChS5J3fJjH6f2YZzd_k8w,13188
|
|
25
25
|
mcp_ticketer/cli/gemini_configure.py,sha256=ZNSA1lBW-itVToza-JxW95Po7daVXKiZAh7lp6pmXMU,9343
|
|
26
26
|
mcp_ticketer/cli/linear_commands.py,sha256=_8f8ze_1MbiUweU6RFHpldgfHLirysIdPjHr2_S0YhI,17319
|
|
27
|
-
mcp_ticketer/cli/main.py,sha256=
|
|
27
|
+
mcp_ticketer/cli/main.py,sha256=BFo7QYU0tTOHWSfTmegzDqflSYFytCE8Jw6QGB7TwaY,74470
|
|
28
28
|
mcp_ticketer/cli/mcp_configure.py,sha256=RzV50UjXgOmvMp-9S0zS39psuvjffVByaMrqrUaAGAM,9594
|
|
29
29
|
mcp_ticketer/cli/migrate_config.py,sha256=MYsr_C5ZxsGg0P13etWTWNrJ_lc6ElRCkzfQADYr3DM,5956
|
|
30
30
|
mcp_ticketer/cli/queue_commands.py,sha256=mm-3H6jmkUGJDyU_E46o9iRpek8tvFCm77F19OtHiZI,7884
|
|
@@ -38,7 +38,7 @@ mcp_ticketer/core/env_loader.py,sha256=VLCQhK50quM5e3LkrAGHD5on5vTBj18Z2IgNMNES1
|
|
|
38
38
|
mcp_ticketer/core/exceptions.py,sha256=H1gUmNiOjVXn4CT-JLQcGXmjWxHaxxdFvwcpJLTrs-U,3621
|
|
39
39
|
mcp_ticketer/core/http_client.py,sha256=s5ikMiwEJ8TJjNn73wu3gv3OdAtyBEpAqPnSroRMW2k,13971
|
|
40
40
|
mcp_ticketer/core/mappers.py,sha256=1aG1jFsHTCwmGRVgOlXW-VOSTGzc86gv7qjDfiR1ups,17462
|
|
41
|
-
mcp_ticketer/core/models.py,sha256=
|
|
41
|
+
mcp_ticketer/core/models.py,sha256=r3BqH0pK2ag2_7c7SSKMZe2G0K7eQID6qy2tkC-GvfI,13155
|
|
42
42
|
mcp_ticketer/core/project_config.py,sha256=5W9YvDBBASEo5fBcSF-rlA1W3LwzkTv6_CEJXxnsOjc,23346
|
|
43
43
|
mcp_ticketer/core/registry.py,sha256=ShYLDPE62KFJpB0kj_zFyQzRxSH3LkQEEuo1jaakb1k,3483
|
|
44
44
|
mcp_ticketer/mcp/__init__.py,sha256=Y05eTzsPk0wH8yKNIM-ekpGjgSDO0bQr0EME-vOP4GE,123
|
|
@@ -54,9 +54,9 @@ mcp_ticketer/queue/queue.py,sha256=PIB_8gOE4rCb5_tBNKw9qD6YhSgH3Ei3IzVrUSY3F_o,1
|
|
|
54
54
|
mcp_ticketer/queue/run_worker.py,sha256=WhoeamL8LKZ66TM8W1PkMPwjF2w_EDFMP-mevs6C1TM,1019
|
|
55
55
|
mcp_ticketer/queue/ticket_registry.py,sha256=FE6W_D8NA-66cJQ6VqghChF3JasYW845JVfEZdiqLbA,15449
|
|
56
56
|
mcp_ticketer/queue/worker.py,sha256=AF6W1bdxWnHiJd6-iBWqTHkZ4lFflsS65CAtgFPR0FA,20983
|
|
57
|
-
mcp_ticketer-0.3.
|
|
58
|
-
mcp_ticketer-0.3.
|
|
59
|
-
mcp_ticketer-0.3.
|
|
60
|
-
mcp_ticketer-0.3.
|
|
61
|
-
mcp_ticketer-0.3.
|
|
62
|
-
mcp_ticketer-0.3.
|
|
57
|
+
mcp_ticketer-0.3.5.dist-info/licenses/LICENSE,sha256=KOVrunjtILSzY-2N8Lqa3-Q8dMaZIG4LrlLTr9UqL08,1073
|
|
58
|
+
mcp_ticketer-0.3.5.dist-info/METADATA,sha256=P_TStpQVYOyHeSxjHnvIQ7WUP3jTbwBDnlL0DmwrORw,13219
|
|
59
|
+
mcp_ticketer-0.3.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
60
|
+
mcp_ticketer-0.3.5.dist-info/entry_points.txt,sha256=o1IxVhnHnBNG7FZzbFq-Whcs1Djbofs0qMjiUYBLx2s,60
|
|
61
|
+
mcp_ticketer-0.3.5.dist-info/top_level.txt,sha256=WnAG4SOT1Vm9tIwl70AbGG_nA217YyV3aWFhxLH2rxw,13
|
|
62
|
+
mcp_ticketer-0.3.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|