mcp-ticketer 0.1.16__py3-none-any.whl → 0.1.19__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/aitrackdown.py +14 -0
- mcp_ticketer/adapters/github.py +34 -0
- mcp_ticketer/adapters/jira.py +34 -0
- mcp_ticketer/adapters/linear.py +159 -50
- mcp_ticketer/cli/main.py +9 -3
- mcp_ticketer/core/adapter.py +9 -0
- mcp_ticketer/core/project_config.py +3 -3
- mcp_ticketer/mcp/server.py +19 -0
- mcp_ticketer/queue/queue.py +20 -5
- mcp_ticketer/queue/worker.py +14 -3
- {mcp_ticketer-0.1.16.dist-info → mcp_ticketer-0.1.19.dist-info}/METADATA +1 -1
- {mcp_ticketer-0.1.16.dist-info → mcp_ticketer-0.1.19.dist-info}/RECORD +17 -17
- {mcp_ticketer-0.1.16.dist-info → mcp_ticketer-0.1.19.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.16.dist-info → mcp_ticketer-0.1.19.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.16.dist-info → mcp_ticketer-0.1.19.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.16.dist-info → mcp_ticketer-0.1.19.dist-info}/top_level.txt +0 -0
mcp_ticketer/__version__.py
CHANGED
|
@@ -40,6 +40,20 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
40
40
|
self.tracker = None
|
|
41
41
|
self.tickets_dir.mkdir(parents=True, exist_ok=True)
|
|
42
42
|
|
|
43
|
+
def validate_credentials(self) -> tuple[bool, str]:
|
|
44
|
+
"""Validate that required credentials are present.
|
|
45
|
+
|
|
46
|
+
AITrackdown is file-based and doesn't require credentials.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
(is_valid, error_message) - Always returns (True, "") for AITrackdown
|
|
50
|
+
"""
|
|
51
|
+
# AITrackdown is file-based and doesn't require API credentials
|
|
52
|
+
# Just verify the base_path is accessible
|
|
53
|
+
if not self.base_path:
|
|
54
|
+
return False, "AITrackdown base_path is required in configuration"
|
|
55
|
+
return True, ""
|
|
56
|
+
|
|
43
57
|
def _get_state_mapping(self) -> Dict[TicketState, str]:
|
|
44
58
|
"""Map universal states to AI-Trackdown states."""
|
|
45
59
|
return {
|
mcp_ticketer/adapters/github.py
CHANGED
|
@@ -191,6 +191,20 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
191
191
|
self._milestones_cache: Optional[List[Dict[str, Any]]] = None
|
|
192
192
|
self._rate_limit: Dict[str, Any] = {}
|
|
193
193
|
|
|
194
|
+
def validate_credentials(self) -> tuple[bool, str]:
|
|
195
|
+
"""Validate that required credentials are present.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
(is_valid, error_message) - Tuple of validation result and error message
|
|
199
|
+
"""
|
|
200
|
+
if not self.token:
|
|
201
|
+
return False, "GITHUB_TOKEN is required but not found. Set it in .env.local or environment."
|
|
202
|
+
if not self.owner:
|
|
203
|
+
return False, "GitHub owner is required in configuration. Set GITHUB_OWNER in .env.local or configure with 'mcp-ticketer init --adapter github --github-owner <owner>'"
|
|
204
|
+
if not self.repo:
|
|
205
|
+
return False, "GitHub repo is required in configuration. Set GITHUB_REPO in .env.local or configure with 'mcp-ticketer init --adapter github --github-repo <repo>'"
|
|
206
|
+
return True, ""
|
|
207
|
+
|
|
194
208
|
def _get_state_mapping(self) -> Dict[TicketState, str]:
|
|
195
209
|
"""Map universal states to GitHub states."""
|
|
196
210
|
return {
|
|
@@ -379,6 +393,11 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
379
393
|
|
|
380
394
|
async def create(self, ticket: Task) -> Task:
|
|
381
395
|
"""Create a new GitHub issue."""
|
|
396
|
+
# Validate credentials before attempting operation
|
|
397
|
+
is_valid, error_message = self.validate_credentials()
|
|
398
|
+
if not is_valid:
|
|
399
|
+
raise ValueError(error_message)
|
|
400
|
+
|
|
382
401
|
# Prepare labels
|
|
383
402
|
labels = ticket.tags.copy() if ticket.tags else []
|
|
384
403
|
|
|
@@ -448,6 +467,11 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
448
467
|
|
|
449
468
|
async def read(self, ticket_id: str) -> Optional[Task]:
|
|
450
469
|
"""Read a GitHub issue by number."""
|
|
470
|
+
# Validate credentials before attempting operation
|
|
471
|
+
is_valid, error_message = self.validate_credentials()
|
|
472
|
+
if not is_valid:
|
|
473
|
+
raise ValueError(error_message)
|
|
474
|
+
|
|
451
475
|
try:
|
|
452
476
|
issue_number = int(ticket_id)
|
|
453
477
|
except ValueError:
|
|
@@ -468,6 +492,11 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
468
492
|
|
|
469
493
|
async def update(self, ticket_id: str, updates: Dict[str, Any]) -> Optional[Task]:
|
|
470
494
|
"""Update a GitHub issue."""
|
|
495
|
+
# Validate credentials before attempting operation
|
|
496
|
+
is_valid, error_message = self.validate_credentials()
|
|
497
|
+
if not is_valid:
|
|
498
|
+
raise ValueError(error_message)
|
|
499
|
+
|
|
471
500
|
try:
|
|
472
501
|
issue_number = int(ticket_id)
|
|
473
502
|
except ValueError:
|
|
@@ -584,6 +613,11 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
584
613
|
|
|
585
614
|
async def delete(self, ticket_id: str) -> bool:
|
|
586
615
|
"""Delete (close) a GitHub issue."""
|
|
616
|
+
# Validate credentials before attempting operation
|
|
617
|
+
is_valid, error_message = self.validate_credentials()
|
|
618
|
+
if not is_valid:
|
|
619
|
+
raise ValueError(error_message)
|
|
620
|
+
|
|
587
621
|
try:
|
|
588
622
|
issue_number = int(ticket_id)
|
|
589
623
|
except ValueError:
|
mcp_ticketer/adapters/jira.py
CHANGED
|
@@ -89,6 +89,20 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
89
89
|
self._issue_types_cache: Dict[str, Any] = {}
|
|
90
90
|
self._custom_fields_cache: Dict[str, Any] = {}
|
|
91
91
|
|
|
92
|
+
def validate_credentials(self) -> tuple[bool, str]:
|
|
93
|
+
"""Validate that required credentials are present.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
(is_valid, error_message) - Tuple of validation result and error message
|
|
97
|
+
"""
|
|
98
|
+
if not self.server:
|
|
99
|
+
return False, "JIRA_SERVER is required but not found. Set it in .env.local or environment."
|
|
100
|
+
if not self.email:
|
|
101
|
+
return False, "JIRA_EMAIL is required but not found. Set it in .env.local or environment."
|
|
102
|
+
if not self.api_token:
|
|
103
|
+
return False, "JIRA_API_TOKEN is required but not found. Set it in .env.local or environment."
|
|
104
|
+
return True, ""
|
|
105
|
+
|
|
92
106
|
def _get_state_mapping(self) -> Dict[TicketState, str]:
|
|
93
107
|
"""Map universal states to common JIRA workflow states."""
|
|
94
108
|
return {
|
|
@@ -457,6 +471,11 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
457
471
|
|
|
458
472
|
async def create(self, ticket: Union[Epic, Task]) -> Union[Epic, Task]:
|
|
459
473
|
"""Create a new JIRA issue."""
|
|
474
|
+
# Validate credentials before attempting operation
|
|
475
|
+
is_valid, error_message = self.validate_credentials()
|
|
476
|
+
if not is_valid:
|
|
477
|
+
raise ValueError(error_message)
|
|
478
|
+
|
|
460
479
|
# Prepare issue fields
|
|
461
480
|
fields = self._ticket_to_issue_fields(ticket)
|
|
462
481
|
|
|
@@ -476,6 +495,11 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
476
495
|
|
|
477
496
|
async def read(self, ticket_id: str) -> Optional[Union[Epic, Task]]:
|
|
478
497
|
"""Read a JIRA issue by key."""
|
|
498
|
+
# Validate credentials before attempting operation
|
|
499
|
+
is_valid, error_message = self.validate_credentials()
|
|
500
|
+
if not is_valid:
|
|
501
|
+
raise ValueError(error_message)
|
|
502
|
+
|
|
479
503
|
try:
|
|
480
504
|
issue = await self._make_request(
|
|
481
505
|
"GET",
|
|
@@ -494,6 +518,11 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
494
518
|
updates: Dict[str, Any]
|
|
495
519
|
) -> Optional[Union[Epic, Task]]:
|
|
496
520
|
"""Update a JIRA issue."""
|
|
521
|
+
# Validate credentials before attempting operation
|
|
522
|
+
is_valid, error_message = self.validate_credentials()
|
|
523
|
+
if not is_valid:
|
|
524
|
+
raise ValueError(error_message)
|
|
525
|
+
|
|
497
526
|
# Read current issue
|
|
498
527
|
current = await self.read(ticket_id)
|
|
499
528
|
if not current:
|
|
@@ -530,6 +559,11 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
530
559
|
|
|
531
560
|
async def delete(self, ticket_id: str) -> bool:
|
|
532
561
|
"""Delete a JIRA issue."""
|
|
562
|
+
# Validate credentials before attempting operation
|
|
563
|
+
is_valid, error_message = self.validate_credentials()
|
|
564
|
+
if not is_valid:
|
|
565
|
+
raise ValueError(error_message)
|
|
566
|
+
|
|
533
567
|
try:
|
|
534
568
|
await self._make_request("DELETE", f"issue/{ticket_id}")
|
|
535
569
|
return True
|
mcp_ticketer/adapters/linear.py
CHANGED
|
@@ -277,8 +277,11 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
277
277
|
config: Configuration with:
|
|
278
278
|
- api_key: Linear API key (or LINEAR_API_KEY env var)
|
|
279
279
|
- workspace: Linear workspace name (optional, for documentation)
|
|
280
|
-
- team_key: Linear team key (
|
|
280
|
+
- team_key: Linear team key (e.g., 'BTA') OR
|
|
281
|
+
- team_id: Linear team UUID (e.g., '02d15669-7351-4451-9719-807576c16049')
|
|
281
282
|
- api_url: Optional Linear API URL
|
|
283
|
+
|
|
284
|
+
Note: Either team_key or team_id is required. If both are provided, team_id takes precedence.
|
|
282
285
|
"""
|
|
283
286
|
super().__init__(config)
|
|
284
287
|
|
|
@@ -288,18 +291,16 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
288
291
|
raise ValueError("Linear API key required (config.api_key or LINEAR_API_KEY env var)")
|
|
289
292
|
|
|
290
293
|
self.workspace = config.get("workspace") # Optional, for documentation
|
|
291
|
-
self.team_key = config.get("team_key")
|
|
292
|
-
if not self.team_key:
|
|
293
|
-
raise ValueError("Linear team_key is required in configuration")
|
|
294
|
-
self.api_url = config.get("api_url", "https://api.linear.app/graphql")
|
|
295
294
|
|
|
296
|
-
#
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
295
|
+
# Support both team_key (short key) and team_id (UUID)
|
|
296
|
+
self.team_key = config.get("team_key") # Short key like "BTA"
|
|
297
|
+
self.team_id_config = config.get("team_id") # UUID like "02d15669-..."
|
|
298
|
+
|
|
299
|
+
# Require at least one team identifier
|
|
300
|
+
if not self.team_key and not self.team_id_config:
|
|
301
|
+
raise ValueError("Either team_key or team_id is required in configuration")
|
|
302
|
+
|
|
303
|
+
self.api_url = config.get("api_url", "https://api.linear.app/graphql")
|
|
303
304
|
|
|
304
305
|
# Caches for frequently used data
|
|
305
306
|
self._team_id: Optional[str] = None
|
|
@@ -314,6 +315,22 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
314
315
|
self._init_lock = asyncio.Lock()
|
|
315
316
|
self._initialized = False
|
|
316
317
|
|
|
318
|
+
def _create_client(self) -> Client:
|
|
319
|
+
"""Create a fresh GraphQL client for each operation.
|
|
320
|
+
|
|
321
|
+
This prevents 'Transport is already connected' errors by ensuring
|
|
322
|
+
each operation gets its own client and transport instance.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Client: Fresh GraphQL client instance
|
|
326
|
+
"""
|
|
327
|
+
transport = HTTPXAsyncTransport(
|
|
328
|
+
url=self.api_url,
|
|
329
|
+
headers={"Authorization": self.api_key},
|
|
330
|
+
timeout=30.0,
|
|
331
|
+
)
|
|
332
|
+
return Client(transport=transport, fetch_schema_from_transport=False)
|
|
333
|
+
|
|
317
334
|
async def initialize(self) -> None:
|
|
318
335
|
"""Initialize adapter by preloading team, states, and labels data concurrently."""
|
|
319
336
|
if self._initialized:
|
|
@@ -347,9 +364,36 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
347
364
|
raise e
|
|
348
365
|
|
|
349
366
|
async def _fetch_team_data(self) -> str:
|
|
350
|
-
"""Fetch team ID.
|
|
367
|
+
"""Fetch team ID.
|
|
368
|
+
|
|
369
|
+
If team_id is configured, validate it exists and return it.
|
|
370
|
+
If team_key is configured, fetch the team_id by key.
|
|
371
|
+
"""
|
|
372
|
+
# If team_id (UUID) is provided, use it directly (preferred)
|
|
373
|
+
if self.team_id_config:
|
|
374
|
+
# Validate that this team ID exists
|
|
375
|
+
query = gql("""
|
|
376
|
+
query GetTeamById($id: String!) {
|
|
377
|
+
team(id: $id) {
|
|
378
|
+
id
|
|
379
|
+
name
|
|
380
|
+
key
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
""")
|
|
384
|
+
|
|
385
|
+
client = self._create_client()
|
|
386
|
+
async with client as session:
|
|
387
|
+
result = await session.execute(query, variable_values={"id": self.team_id_config})
|
|
388
|
+
|
|
389
|
+
if not result.get("team"):
|
|
390
|
+
raise ValueError(f"Team with ID '{self.team_id_config}' not found")
|
|
391
|
+
|
|
392
|
+
return result["team"]["id"]
|
|
393
|
+
|
|
394
|
+
# Otherwise, fetch team ID by key
|
|
351
395
|
query = gql("""
|
|
352
|
-
query
|
|
396
|
+
query GetTeamByKey($key: String!) {
|
|
353
397
|
teams(filter: { key: { eq: $key } }) {
|
|
354
398
|
nodes {
|
|
355
399
|
id
|
|
@@ -360,7 +404,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
360
404
|
}
|
|
361
405
|
""")
|
|
362
406
|
|
|
363
|
-
|
|
407
|
+
client = self._create_client()
|
|
408
|
+
async with client as session:
|
|
364
409
|
result = await session.execute(query, variable_values={"key": self.team_key})
|
|
365
410
|
|
|
366
411
|
if not result["teams"]["nodes"]:
|
|
@@ -384,7 +429,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
384
429
|
}
|
|
385
430
|
""")
|
|
386
431
|
|
|
387
|
-
|
|
432
|
+
client = self._create_client()
|
|
433
|
+
async with client as session:
|
|
388
434
|
result = await session.execute(query, variable_values={"teamId": team_id})
|
|
389
435
|
|
|
390
436
|
workflow_states = {}
|
|
@@ -410,7 +456,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
410
456
|
}
|
|
411
457
|
""")
|
|
412
458
|
|
|
413
|
-
|
|
459
|
+
client = self._create_client()
|
|
460
|
+
async with client as session:
|
|
414
461
|
result = await session.execute(query, variable_values={"teamId": team_id})
|
|
415
462
|
|
|
416
463
|
return {label["name"]: label["id"] for label in result["issueLabels"]["nodes"]}
|
|
@@ -451,7 +498,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
451
498
|
}
|
|
452
499
|
""")
|
|
453
500
|
|
|
454
|
-
|
|
501
|
+
client = self._create_client()
|
|
502
|
+
async with client as session:
|
|
455
503
|
result = await session.execute(
|
|
456
504
|
search_query,
|
|
457
505
|
variable_values={"name": name, "teamId": team_id}
|
|
@@ -481,7 +529,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
481
529
|
if color:
|
|
482
530
|
label_input["color"] = color
|
|
483
531
|
|
|
484
|
-
|
|
532
|
+
client = self._create_client()
|
|
533
|
+
async with client as session:
|
|
485
534
|
result = await session.execute(
|
|
486
535
|
create_query,
|
|
487
536
|
variable_values={"input": label_input}
|
|
@@ -510,7 +559,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
510
559
|
}
|
|
511
560
|
""")
|
|
512
561
|
|
|
513
|
-
|
|
562
|
+
client = self._create_client()
|
|
563
|
+
async with client as session:
|
|
514
564
|
result = await session.execute(query, variable_values={"email": email})
|
|
515
565
|
|
|
516
566
|
if result["users"]["nodes"]:
|
|
@@ -520,6 +570,18 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
520
570
|
|
|
521
571
|
return None
|
|
522
572
|
|
|
573
|
+
def validate_credentials(self) -> tuple[bool, str]:
|
|
574
|
+
"""Validate that required credentials are present.
|
|
575
|
+
|
|
576
|
+
Returns:
|
|
577
|
+
(is_valid, error_message) - Tuple of validation result and error message
|
|
578
|
+
"""
|
|
579
|
+
if not self.api_key:
|
|
580
|
+
return False, "LINEAR_API_KEY is required but not found. Set it in .env.local or environment."
|
|
581
|
+
if not self.team_key and not self.team_id_config:
|
|
582
|
+
return False, "Either Linear team_key or team_id is required in configuration. Set it in .mcp-ticketer/config.json"
|
|
583
|
+
return True, ""
|
|
584
|
+
|
|
523
585
|
def _get_state_mapping(self) -> Dict[TicketState, str]:
|
|
524
586
|
"""Get mapping from universal states to Linear state types.
|
|
525
587
|
|
|
@@ -711,6 +773,11 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
711
773
|
|
|
712
774
|
async def create(self, ticket: Task) -> Task:
|
|
713
775
|
"""Create a new Linear issue with full field support."""
|
|
776
|
+
# Validate credentials before attempting operation
|
|
777
|
+
is_valid, error_message = self.validate_credentials()
|
|
778
|
+
if not is_valid:
|
|
779
|
+
raise ValueError(error_message)
|
|
780
|
+
|
|
714
781
|
team_id = await self._ensure_team_id()
|
|
715
782
|
states = await self._get_workflow_states()
|
|
716
783
|
|
|
@@ -775,7 +842,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
775
842
|
}
|
|
776
843
|
}
|
|
777
844
|
""")
|
|
778
|
-
|
|
845
|
+
client = self._create_client()
|
|
846
|
+
async with client as session:
|
|
779
847
|
parent_result = await session.execute(
|
|
780
848
|
parent_query,
|
|
781
849
|
variable_values={"identifier": ticket.parent_issue}
|
|
@@ -807,7 +875,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
807
875
|
}
|
|
808
876
|
""")
|
|
809
877
|
|
|
810
|
-
|
|
878
|
+
client = self._create_client()
|
|
879
|
+
async with client as session:
|
|
811
880
|
result = await session.execute(
|
|
812
881
|
create_query,
|
|
813
882
|
variable_values={"input": issue_input}
|
|
@@ -821,6 +890,11 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
821
890
|
|
|
822
891
|
async def read(self, ticket_id: str) -> Optional[Task]:
|
|
823
892
|
"""Read a Linear issue by identifier with full details."""
|
|
893
|
+
# Validate credentials before attempting operation
|
|
894
|
+
is_valid, error_message = self.validate_credentials()
|
|
895
|
+
if not is_valid:
|
|
896
|
+
raise ValueError(error_message)
|
|
897
|
+
|
|
824
898
|
query = gql(ALL_FRAGMENTS + """
|
|
825
899
|
query GetIssue($identifier: String!) {
|
|
826
900
|
issue(id: $identifier) {
|
|
@@ -830,7 +904,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
830
904
|
""")
|
|
831
905
|
|
|
832
906
|
try:
|
|
833
|
-
|
|
907
|
+
client = self._create_client()
|
|
908
|
+
async with client as session:
|
|
834
909
|
result = await session.execute(
|
|
835
910
|
query,
|
|
836
911
|
variable_values={"identifier": ticket_id}
|
|
@@ -846,6 +921,11 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
846
921
|
|
|
847
922
|
async def update(self, ticket_id: str, updates: Dict[str, Any]) -> Optional[Task]:
|
|
848
923
|
"""Update a Linear issue with comprehensive field support."""
|
|
924
|
+
# Validate credentials before attempting operation
|
|
925
|
+
is_valid, error_message = self.validate_credentials()
|
|
926
|
+
if not is_valid:
|
|
927
|
+
raise ValueError(error_message)
|
|
928
|
+
|
|
849
929
|
# First get the Linear internal ID
|
|
850
930
|
query = gql("""
|
|
851
931
|
query GetIssueId($identifier: String!) {
|
|
@@ -855,7 +935,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
855
935
|
}
|
|
856
936
|
""")
|
|
857
937
|
|
|
858
|
-
|
|
938
|
+
client = self._create_client()
|
|
939
|
+
async with client as session:
|
|
859
940
|
result = await session.execute(
|
|
860
941
|
query,
|
|
861
942
|
variable_values={"identifier": ticket_id}
|
|
@@ -931,7 +1012,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
931
1012
|
}
|
|
932
1013
|
""")
|
|
933
1014
|
|
|
934
|
-
|
|
1015
|
+
client = self._create_client()
|
|
1016
|
+
async with client as session:
|
|
935
1017
|
result = await session.execute(
|
|
936
1018
|
update_query,
|
|
937
1019
|
variable_values={"id": linear_id, "input": update_input}
|
|
@@ -944,6 +1026,11 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
944
1026
|
|
|
945
1027
|
async def delete(self, ticket_id: str) -> bool:
|
|
946
1028
|
"""Archive (soft delete) a Linear issue."""
|
|
1029
|
+
# Validate credentials before attempting operation
|
|
1030
|
+
is_valid, error_message = self.validate_credentials()
|
|
1031
|
+
if not is_valid:
|
|
1032
|
+
raise ValueError(error_message)
|
|
1033
|
+
|
|
947
1034
|
# Get Linear ID
|
|
948
1035
|
query = gql("""
|
|
949
1036
|
query GetIssueId($identifier: String!) {
|
|
@@ -953,7 +1040,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
953
1040
|
}
|
|
954
1041
|
""")
|
|
955
1042
|
|
|
956
|
-
|
|
1043
|
+
client = self._create_client()
|
|
1044
|
+
async with client as session:
|
|
957
1045
|
result = await session.execute(
|
|
958
1046
|
query,
|
|
959
1047
|
variable_values={"identifier": ticket_id}
|
|
@@ -973,7 +1061,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
973
1061
|
}
|
|
974
1062
|
""")
|
|
975
1063
|
|
|
976
|
-
|
|
1064
|
+
client = self._create_client()
|
|
1065
|
+
async with client as session:
|
|
977
1066
|
result = await session.execute(
|
|
978
1067
|
archive_query,
|
|
979
1068
|
variable_values={"id": linear_id}
|
|
@@ -1069,7 +1158,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1069
1158
|
}
|
|
1070
1159
|
""")
|
|
1071
1160
|
|
|
1072
|
-
|
|
1161
|
+
client = self._create_client()
|
|
1162
|
+
async with client as session:
|
|
1073
1163
|
result = await session.execute(
|
|
1074
1164
|
query,
|
|
1075
1165
|
variable_values={
|
|
@@ -1143,7 +1233,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1143
1233
|
}
|
|
1144
1234
|
""")
|
|
1145
1235
|
|
|
1146
|
-
|
|
1236
|
+
client = self._create_client()
|
|
1237
|
+
async with client as session:
|
|
1147
1238
|
result = await session.execute(
|
|
1148
1239
|
search_query,
|
|
1149
1240
|
variable_values={
|
|
@@ -1183,7 +1274,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1183
1274
|
}
|
|
1184
1275
|
""")
|
|
1185
1276
|
|
|
1186
|
-
|
|
1277
|
+
client = self._create_client()
|
|
1278
|
+
async with client as session:
|
|
1187
1279
|
result = await session.execute(
|
|
1188
1280
|
query,
|
|
1189
1281
|
variable_values={"identifier": comment.ticket_id}
|
|
@@ -1215,7 +1307,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1215
1307
|
if comment.metadata and "parent_comment_id" in comment.metadata:
|
|
1216
1308
|
comment_input["parentId"] = comment.metadata["parent_comment_id"]
|
|
1217
1309
|
|
|
1218
|
-
|
|
1310
|
+
client = self._create_client()
|
|
1311
|
+
async with client as session:
|
|
1219
1312
|
result = await session.execute(
|
|
1220
1313
|
create_comment_query,
|
|
1221
1314
|
variable_values={"input": comment_input}
|
|
@@ -1260,7 +1353,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1260
1353
|
""")
|
|
1261
1354
|
|
|
1262
1355
|
try:
|
|
1263
|
-
|
|
1356
|
+
client = self._create_client()
|
|
1357
|
+
async with client as session:
|
|
1264
1358
|
result = await session.execute(
|
|
1265
1359
|
query,
|
|
1266
1360
|
variable_values={
|
|
@@ -1316,7 +1410,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1316
1410
|
if description:
|
|
1317
1411
|
project_input["description"] = description
|
|
1318
1412
|
|
|
1319
|
-
|
|
1413
|
+
client = self._create_client()
|
|
1414
|
+
async with client as session:
|
|
1320
1415
|
result = await session.execute(
|
|
1321
1416
|
create_query,
|
|
1322
1417
|
variable_values={"input": project_input}
|
|
@@ -1357,7 +1452,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1357
1452
|
}
|
|
1358
1453
|
""")
|
|
1359
1454
|
|
|
1360
|
-
|
|
1455
|
+
client = self._create_client()
|
|
1456
|
+
async with client as session:
|
|
1361
1457
|
result = await session.execute(
|
|
1362
1458
|
query,
|
|
1363
1459
|
variable_values={"filter": cycle_filter}
|
|
@@ -1392,7 +1488,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1392
1488
|
}
|
|
1393
1489
|
""")
|
|
1394
1490
|
|
|
1395
|
-
|
|
1491
|
+
client = self._create_client()
|
|
1492
|
+
async with client as session:
|
|
1396
1493
|
result = await session.execute(
|
|
1397
1494
|
create_query,
|
|
1398
1495
|
variable_values={
|
|
@@ -1470,7 +1567,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1470
1567
|
},
|
|
1471
1568
|
}
|
|
1472
1569
|
|
|
1473
|
-
|
|
1570
|
+
client = self._create_client()
|
|
1571
|
+
async with client as session:
|
|
1474
1572
|
result = await session.execute(
|
|
1475
1573
|
create_query,
|
|
1476
1574
|
variable_values={"input": attachment_input}
|
|
@@ -1569,7 +1667,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1569
1667
|
raise ValueError(f"Could not find Linear ID for issue {ticket_id}")
|
|
1570
1668
|
linear_id = search_result["id"]
|
|
1571
1669
|
|
|
1572
|
-
|
|
1670
|
+
client = self._create_client()
|
|
1671
|
+
async with client as session:
|
|
1573
1672
|
result = await session.execute(
|
|
1574
1673
|
update_query,
|
|
1575
1674
|
variable_values={
|
|
@@ -1616,7 +1715,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1616
1715
|
""")
|
|
1617
1716
|
|
|
1618
1717
|
try:
|
|
1619
|
-
|
|
1718
|
+
client = self._create_client()
|
|
1719
|
+
async with client as session:
|
|
1620
1720
|
result = await session.execute(
|
|
1621
1721
|
search_query,
|
|
1622
1722
|
variable_values={"identifier": identifier}
|
|
@@ -1669,7 +1769,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1669
1769
|
if "lead_id" in kwargs:
|
|
1670
1770
|
project_input["leadId"] = kwargs["lead_id"]
|
|
1671
1771
|
|
|
1672
|
-
|
|
1772
|
+
client = self._create_client()
|
|
1773
|
+
async with client as session:
|
|
1673
1774
|
result = await session.execute(
|
|
1674
1775
|
create_query,
|
|
1675
1776
|
variable_values={"input": project_input}
|
|
@@ -1699,7 +1800,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1699
1800
|
""")
|
|
1700
1801
|
|
|
1701
1802
|
try:
|
|
1702
|
-
|
|
1803
|
+
client = self._create_client()
|
|
1804
|
+
async with client as session:
|
|
1703
1805
|
result = await session.execute(
|
|
1704
1806
|
query,
|
|
1705
1807
|
variable_values={"id": epic_id}
|
|
@@ -1748,7 +1850,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1748
1850
|
}
|
|
1749
1851
|
""")
|
|
1750
1852
|
|
|
1751
|
-
|
|
1853
|
+
client = self._create_client()
|
|
1854
|
+
async with client as session:
|
|
1752
1855
|
result = await session.execute(
|
|
1753
1856
|
query,
|
|
1754
1857
|
variable_values={
|
|
@@ -1815,7 +1918,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1815
1918
|
""")
|
|
1816
1919
|
|
|
1817
1920
|
try:
|
|
1818
|
-
|
|
1921
|
+
client = self._create_client()
|
|
1922
|
+
async with client as session:
|
|
1819
1923
|
result = await session.execute(
|
|
1820
1924
|
query,
|
|
1821
1925
|
variable_values={"projectId": epic_id, "first": 100}
|
|
@@ -1868,7 +1972,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1868
1972
|
}
|
|
1869
1973
|
""")
|
|
1870
1974
|
|
|
1871
|
-
|
|
1975
|
+
client = self._create_client()
|
|
1976
|
+
async with client as session:
|
|
1872
1977
|
parent_result = await session.execute(
|
|
1873
1978
|
parent_query,
|
|
1874
1979
|
variable_values={"identifier": parent_id}
|
|
@@ -1933,7 +2038,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1933
2038
|
}
|
|
1934
2039
|
""")
|
|
1935
2040
|
|
|
1936
|
-
|
|
2041
|
+
client = self._create_client()
|
|
2042
|
+
async with client as session:
|
|
1937
2043
|
result = await session.execute(
|
|
1938
2044
|
create_query,
|
|
1939
2045
|
variable_values={"input": issue_input}
|
|
@@ -1967,7 +2073,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1967
2073
|
""")
|
|
1968
2074
|
|
|
1969
2075
|
try:
|
|
1970
|
-
|
|
2076
|
+
client = self._create_client()
|
|
2077
|
+
async with client as session:
|
|
1971
2078
|
result = await session.execute(
|
|
1972
2079
|
query,
|
|
1973
2080
|
variable_values={"identifier": issue_id}
|
|
@@ -1988,11 +2095,13 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1988
2095
|
return []
|
|
1989
2096
|
|
|
1990
2097
|
async def close(self) -> None:
|
|
1991
|
-
"""Close the GraphQL client connection.
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
2098
|
+
"""Close the GraphQL client connection.
|
|
2099
|
+
|
|
2100
|
+
Since we create fresh clients for each operation, there's no persistent
|
|
2101
|
+
connection to close. Each client's transport is automatically closed when
|
|
2102
|
+
the async context manager exits.
|
|
2103
|
+
"""
|
|
2104
|
+
pass
|
|
1996
2105
|
|
|
1997
2106
|
|
|
1998
2107
|
# Register the adapter
|
mcp_ticketer/cli/main.py
CHANGED
|
@@ -70,18 +70,24 @@ class AdapterType(str, Enum):
|
|
|
70
70
|
GITHUB = "github"
|
|
71
71
|
|
|
72
72
|
|
|
73
|
-
def load_config() -> dict:
|
|
73
|
+
def load_config(project_dir: Optional[Path] = None) -> dict:
|
|
74
74
|
"""Load configuration from file.
|
|
75
75
|
|
|
76
|
+
Args:
|
|
77
|
+
project_dir: Optional project directory to load config from
|
|
78
|
+
|
|
76
79
|
Resolution order:
|
|
77
|
-
1. Project-specific config (.mcp-ticketer/config.json in cwd)
|
|
80
|
+
1. Project-specific config (.mcp-ticketer/config.json in project_dir or cwd)
|
|
78
81
|
2. Global config (~/.mcp-ticketer/config.json)
|
|
79
82
|
|
|
80
83
|
Returns:
|
|
81
84
|
Configuration dictionary
|
|
82
85
|
"""
|
|
86
|
+
# Use provided project_dir or current working directory
|
|
87
|
+
base_dir = project_dir or Path.cwd()
|
|
88
|
+
|
|
83
89
|
# Check project-specific config first
|
|
84
|
-
project_config =
|
|
90
|
+
project_config = base_dir / ".mcp-ticketer" / "config.json"
|
|
85
91
|
if project_config.exists():
|
|
86
92
|
try:
|
|
87
93
|
with open(project_config, "r") as f:
|
mcp_ticketer/core/adapter.py
CHANGED
|
@@ -29,6 +29,15 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
29
29
|
"""
|
|
30
30
|
pass
|
|
31
31
|
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def validate_credentials(self) -> tuple[bool, str]:
|
|
34
|
+
"""Validate that required credentials are present.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
(is_valid, error_message) - Tuple of validation result and error message
|
|
38
|
+
"""
|
|
39
|
+
pass
|
|
40
|
+
|
|
32
41
|
@abstractmethod
|
|
33
42
|
async def create(self, ticket: T) -> T:
|
|
34
43
|
"""Create a new ticket.
|
|
@@ -211,9 +211,9 @@ class ConfigValidator:
|
|
|
211
211
|
if field not in config or not config[field]:
|
|
212
212
|
return False, f"Linear config missing required field: {field}"
|
|
213
213
|
|
|
214
|
-
#
|
|
215
|
-
if not config.get("team_id"):
|
|
216
|
-
|
|
214
|
+
# Require either team_key or team_id (team_id is preferred)
|
|
215
|
+
if not config.get("team_key") and not config.get("team_id"):
|
|
216
|
+
return False, "Linear config requires either team_key (short key like 'BTA') or team_id (UUID)"
|
|
217
217
|
|
|
218
218
|
return True, None
|
|
219
219
|
|
mcp_ticketer/mcp/server.py
CHANGED
|
@@ -4,12 +4,31 @@ import asyncio
|
|
|
4
4
|
import json
|
|
5
5
|
import sys
|
|
6
6
|
from typing import Any, Dict, List, Optional
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from dotenv import load_dotenv
|
|
7
9
|
|
|
8
10
|
from ..core import Task, TicketState, Priority, AdapterRegistry
|
|
9
11
|
from ..core.models import SearchQuery, Comment
|
|
10
12
|
from ..adapters import AITrackdownAdapter
|
|
11
13
|
from ..queue import Queue, QueueStatus, WorkerManager
|
|
12
14
|
|
|
15
|
+
# Load environment variables early (prioritize .env.local)
|
|
16
|
+
# Check for .env.local first (takes precedence)
|
|
17
|
+
env_local_file = Path.cwd() / ".env.local"
|
|
18
|
+
if env_local_file.exists():
|
|
19
|
+
load_dotenv(env_local_file, override=True)
|
|
20
|
+
sys.stderr.write(f"[MCP Server] Loaded environment from: {env_local_file}\n")
|
|
21
|
+
else:
|
|
22
|
+
# Fall back to .env
|
|
23
|
+
env_file = Path.cwd() / ".env"
|
|
24
|
+
if env_file.exists():
|
|
25
|
+
load_dotenv(env_file, override=True)
|
|
26
|
+
sys.stderr.write(f"[MCP Server] Loaded environment from: {env_file}\n")
|
|
27
|
+
else:
|
|
28
|
+
# Try default dotenv loading (searches upward)
|
|
29
|
+
load_dotenv(override=True)
|
|
30
|
+
sys.stderr.write("[MCP Server] Loaded environment from default search path\n")
|
|
31
|
+
|
|
13
32
|
|
|
14
33
|
class MCPTicketServer:
|
|
15
34
|
"""MCP server for ticket operations over stdio."""
|
mcp_ticketer/queue/queue.py
CHANGED
|
@@ -32,6 +32,7 @@ class QueueItem:
|
|
|
32
32
|
error_message: Optional[str] = None
|
|
33
33
|
retry_count: int = 0
|
|
34
34
|
result: Optional[Dict[str, Any]] = None
|
|
35
|
+
project_dir: Optional[str] = None
|
|
35
36
|
|
|
36
37
|
def to_dict(self) -> dict:
|
|
37
38
|
"""Convert to dictionary for storage."""
|
|
@@ -54,7 +55,8 @@ class QueueItem:
|
|
|
54
55
|
processed_at=datetime.fromisoformat(row[6]) if row[6] else None,
|
|
55
56
|
error_message=row[7],
|
|
56
57
|
retry_count=row[8],
|
|
57
|
-
result=json.loads(row[9]) if row[9] else None
|
|
58
|
+
result=json.loads(row[9]) if row[9] else None,
|
|
59
|
+
project_dir=row[10] if len(row) > 10 else None
|
|
58
60
|
)
|
|
59
61
|
|
|
60
62
|
|
|
@@ -109,31 +111,43 @@ class Queue:
|
|
|
109
111
|
ON queue(adapter)
|
|
110
112
|
''')
|
|
111
113
|
|
|
114
|
+
# Migration: Add project_dir column if it doesn't exist
|
|
115
|
+
cursor = conn.execute("PRAGMA table_info(queue)")
|
|
116
|
+
columns = [row[1] for row in cursor.fetchall()]
|
|
117
|
+
if 'project_dir' not in columns:
|
|
118
|
+
conn.execute('ALTER TABLE queue ADD COLUMN project_dir TEXT')
|
|
119
|
+
|
|
112
120
|
conn.commit()
|
|
113
121
|
|
|
114
122
|
def add(self,
|
|
115
123
|
ticket_data: Dict[str, Any],
|
|
116
124
|
adapter: str,
|
|
117
|
-
operation: str
|
|
125
|
+
operation: str,
|
|
126
|
+
project_dir: Optional[str] = None) -> str:
|
|
118
127
|
"""Add item to queue.
|
|
119
128
|
|
|
120
129
|
Args:
|
|
121
130
|
ticket_data: The ticket data for the operation
|
|
122
131
|
adapter: Name of the adapter to use
|
|
123
132
|
operation: Operation to perform (create, update, delete, etc.)
|
|
133
|
+
project_dir: Project directory for config resolution (defaults to current directory)
|
|
124
134
|
|
|
125
135
|
Returns:
|
|
126
136
|
Queue ID for tracking
|
|
127
137
|
"""
|
|
128
138
|
queue_id = f"Q-{uuid.uuid4().hex[:8].upper()}"
|
|
129
139
|
|
|
140
|
+
# Default to current working directory if not provided
|
|
141
|
+
if project_dir is None:
|
|
142
|
+
project_dir = str(Path.cwd())
|
|
143
|
+
|
|
130
144
|
with self._lock:
|
|
131
145
|
with sqlite3.connect(self.db_path) as conn:
|
|
132
146
|
conn.execute('''
|
|
133
147
|
INSERT INTO queue (
|
|
134
148
|
id, ticket_data, adapter, operation,
|
|
135
|
-
status, created_at, retry_count
|
|
136
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
149
|
+
status, created_at, retry_count, project_dir
|
|
150
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
137
151
|
''', (
|
|
138
152
|
queue_id,
|
|
139
153
|
json.dumps(ticket_data),
|
|
@@ -141,7 +155,8 @@ class Queue:
|
|
|
141
155
|
operation,
|
|
142
156
|
QueueStatus.PENDING.value,
|
|
143
157
|
datetime.now().isoformat(),
|
|
144
|
-
0
|
|
158
|
+
0,
|
|
159
|
+
project_dir
|
|
145
160
|
))
|
|
146
161
|
conn.commit()
|
|
147
162
|
|
mcp_ticketer/queue/worker.py
CHANGED
|
@@ -303,15 +303,26 @@ class Worker:
|
|
|
303
303
|
Returns:
|
|
304
304
|
Adapter instance
|
|
305
305
|
"""
|
|
306
|
-
# Load configuration
|
|
306
|
+
# Load configuration from the project directory where the item was created
|
|
307
307
|
from ..cli.main import load_config
|
|
308
|
+
from pathlib import Path
|
|
309
|
+
import os
|
|
310
|
+
|
|
311
|
+
# Use item's project_dir if available, otherwise use current directory
|
|
312
|
+
project_path = Path(item.project_dir) if item.project_dir else None
|
|
308
313
|
|
|
309
|
-
|
|
314
|
+
# Load environment variables from project directory's .env.local if it exists
|
|
315
|
+
if project_path:
|
|
316
|
+
env_file = project_path / ".env.local"
|
|
317
|
+
if env_file.exists():
|
|
318
|
+
logger.debug(f"Loading environment from {env_file}")
|
|
319
|
+
load_dotenv(env_file)
|
|
320
|
+
|
|
321
|
+
config = load_config(project_dir=project_path)
|
|
310
322
|
adapters_config = config.get("adapters", {})
|
|
311
323
|
adapter_config = adapters_config.get(item.adapter, {})
|
|
312
324
|
|
|
313
325
|
# Add environment variables for authentication
|
|
314
|
-
import os
|
|
315
326
|
if item.adapter == "linear":
|
|
316
327
|
if not adapter_config.get("api_key"):
|
|
317
328
|
adapter_config["api_key"] = os.getenv("LINEAR_API_KEY")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-ticketer
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.19
|
|
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,42 +1,42 @@
|
|
|
1
1
|
mcp_ticketer/__init__.py,sha256=ayPQdFr6msypD06_G96a1H0bdFCT1m1wDtv8MZBpY4I,496
|
|
2
|
-
mcp_ticketer/__version__.py,sha256=
|
|
2
|
+
mcp_ticketer/__version__.py,sha256=kw2I3NwlDBDu_l0kuiTcEjSESqAT8gllqD-5y3g9No4,1115
|
|
3
3
|
mcp_ticketer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
mcp_ticketer/adapters/__init__.py,sha256=K_1egvhHb5F_7yFceUx2YzPGEoc7vX-q8dMVaS4K6gw,356
|
|
5
|
-
mcp_ticketer/adapters/aitrackdown.py,sha256=
|
|
6
|
-
mcp_ticketer/adapters/github.py,sha256=
|
|
5
|
+
mcp_ticketer/adapters/aitrackdown.py,sha256=916SpzQcG6gSdQit5Ptm9AdGOsZFXpt9nNWlRjCReMY,15924
|
|
6
|
+
mcp_ticketer/adapters/github.py,sha256=NdUPaSlOEi4zZN_VBvAjSJANJhp1IBwdOkkF6fGbaKs,45410
|
|
7
7
|
mcp_ticketer/adapters/hybrid.py,sha256=H9B-pfWmDKXO3GgzxB8undEcZTMzLz_1a6zWhj7xfR0,18556
|
|
8
|
-
mcp_ticketer/adapters/jira.py,sha256=
|
|
9
|
-
mcp_ticketer/adapters/linear.py,sha256=
|
|
8
|
+
mcp_ticketer/adapters/jira.py,sha256=jxoQS22wjOl1FhsYiGK-r1pLXOenUmbe5wa0ehD6xDg,30373
|
|
9
|
+
mcp_ticketer/adapters/linear.py,sha256=ewGTpvJmyTBJd73PPQBM6ijwVsacXZ4oRAigh_RAdAA,69315
|
|
10
10
|
mcp_ticketer/cache/__init__.py,sha256=MSi3GLXancfP2-edPC9TFAJk7r0j6H5-XmpMHnkGPbI,137
|
|
11
11
|
mcp_ticketer/cache/memory.py,sha256=gTzv-xF7qGfiYVUjG7lnzo0ZcqgXQajMl4NAYUcaytg,5133
|
|
12
12
|
mcp_ticketer/cli/__init__.py,sha256=YeljyLtv906TqkvRuEPhmKO-Uk0CberQ9I6kx1tx2UA,88
|
|
13
13
|
mcp_ticketer/cli/configure.py,sha256=etFutvc0QpaVDMOsZiiN7wKuaT98Od1Tj9W6lsEWw5A,16351
|
|
14
14
|
mcp_ticketer/cli/discover.py,sha256=putWrGcctUH8K0fOMtr9MZA9VnWoXzbtoe7mXSkDxhg,13156
|
|
15
|
-
mcp_ticketer/cli/main.py,sha256=
|
|
15
|
+
mcp_ticketer/cli/main.py,sha256=T8m3mBcBqvOmW_bVU-RsSfzGY1QWZN9c91tfcOJ8KB0,40656
|
|
16
16
|
mcp_ticketer/cli/mcp_configure.py,sha256=1WbBdF25OvxfAGcjxtYa9xmBOGEPQu-wh_nkefmWjMQ,9352
|
|
17
17
|
mcp_ticketer/cli/migrate_config.py,sha256=iZIstnlr9vkhiW_MlnSyJOkMi4KHQqrZ6Hz1ECD_VUk,6045
|
|
18
18
|
mcp_ticketer/cli/queue_commands.py,sha256=f3pEHKZ43dBHEIoCBvdfvjfMB9_WJltps9ATwTzorY0,8160
|
|
19
19
|
mcp_ticketer/cli/utils.py,sha256=cdP-7GHtELAPZtqInUC24k_SAnRbIRkafIP3T4kMZDM,19509
|
|
20
20
|
mcp_ticketer/core/__init__.py,sha256=qpCZveQMyqU2JvYG9MG_c6X35z_VoSmjWdXGcUZqqmA,348
|
|
21
|
-
mcp_ticketer/core/adapter.py,sha256=
|
|
21
|
+
mcp_ticketer/core/adapter.py,sha256=W87W-hEmgCxw5BkvaFlCGZtouN49aW2KHND53zgg6-c,10339
|
|
22
22
|
mcp_ticketer/core/config.py,sha256=9a2bksbcFr7KXeHSPY6KoSP5Pzt54utYPCmbM-1QKmk,13932
|
|
23
23
|
mcp_ticketer/core/env_discovery.py,sha256=SPoyq_y5j-3gJG5gYNVjCIIrbdzimOdDbTYySmQWZOA,17536
|
|
24
24
|
mcp_ticketer/core/http_client.py,sha256=RM9CEMNcuRb-FxhAijmM_FeBMgxgh1OII9HIPBdJue0,13855
|
|
25
25
|
mcp_ticketer/core/mappers.py,sha256=8I4jcqDqoQEdWlteDMpVeVF3Wo0fDCkmFPRr8oNv8gA,16933
|
|
26
26
|
mcp_ticketer/core/models.py,sha256=GhuTitY6t_QlqfEUvWT6Q2zvY7qAtx_SQyCMMn8iYkk,6473
|
|
27
|
-
mcp_ticketer/core/project_config.py,sha256=
|
|
27
|
+
mcp_ticketer/core/project_config.py,sha256=dc_sGE6ds_WYBdwY2s6onWP07umTQ_TZBRmIL_ZrMpI,22446
|
|
28
28
|
mcp_ticketer/core/registry.py,sha256=fwje0fnjp0YKPZ0SrVWk82SMNLs7CD0JlHQmx7SigNo,3537
|
|
29
29
|
mcp_ticketer/mcp/__init__.py,sha256=Bvzof9vBu6VwcXcIZK8RgKv6ycRV9tDlO-9TUmd8zqQ,122
|
|
30
|
-
mcp_ticketer/mcp/server.py,sha256=
|
|
30
|
+
mcp_ticketer/mcp/server.py,sha256=CC1iaeugUbiVrNvNgOgm2mRb4AW-5e0X2ygLjH8I6mM,34835
|
|
31
31
|
mcp_ticketer/queue/__init__.py,sha256=xHBoUwor8ZdO8bIHc7nP25EsAp5Si5Co4g_8ybb7fes,230
|
|
32
32
|
mcp_ticketer/queue/__main__.py,sha256=kQd6iOCKbbFqpRdbIRavuI4_G7-oE898JE4a0yLEYPE,108
|
|
33
33
|
mcp_ticketer/queue/manager.py,sha256=79AH9oUxdBXH3lmJ3kIlFf2GQkWHL6XB6u5JqVWPq60,7571
|
|
34
|
-
mcp_ticketer/queue/queue.py,sha256=
|
|
34
|
+
mcp_ticketer/queue/queue.py,sha256=mXCUwlayqEHDB6IN8RvxSQpVdKcrlSww40VS_4i9lj8,12292
|
|
35
35
|
mcp_ticketer/queue/run_worker.py,sha256=HFoykfDpOoz8OUxWbQ2Fka_UlGrYwjPVZ-DEimGFH9o,802
|
|
36
|
-
mcp_ticketer/queue/worker.py,sha256=
|
|
37
|
-
mcp_ticketer-0.1.
|
|
38
|
-
mcp_ticketer-0.1.
|
|
39
|
-
mcp_ticketer-0.1.
|
|
40
|
-
mcp_ticketer-0.1.
|
|
41
|
-
mcp_ticketer-0.1.
|
|
42
|
-
mcp_ticketer-0.1.
|
|
36
|
+
mcp_ticketer/queue/worker.py,sha256=h0Y3l51Ld5zpWd-HoQ3sPlgcGRJM1kiId3aKSqJSars,14588
|
|
37
|
+
mcp_ticketer-0.1.19.dist-info/licenses/LICENSE,sha256=KOVrunjtILSzY-2N8Lqa3-Q8dMaZIG4LrlLTr9UqL08,1073
|
|
38
|
+
mcp_ticketer-0.1.19.dist-info/METADATA,sha256=1ZNJdi1dFE0as6QGBewuoYdIAtkjvptEslgraOZfR_U,11211
|
|
39
|
+
mcp_ticketer-0.1.19.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
40
|
+
mcp_ticketer-0.1.19.dist-info/entry_points.txt,sha256=o1IxVhnHnBNG7FZzbFq-Whcs1Djbofs0qMjiUYBLx2s,60
|
|
41
|
+
mcp_ticketer-0.1.19.dist-info/top_level.txt,sha256=WnAG4SOT1Vm9tIwl70AbGG_nA217YyV3aWFhxLH2rxw,13
|
|
42
|
+
mcp_ticketer-0.1.19.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|