mcp-ticketer 0.1.21__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.

Files changed (42) hide show
  1. mcp_ticketer/__init__.py +7 -7
  2. mcp_ticketer/__version__.py +4 -2
  3. mcp_ticketer/adapters/__init__.py +4 -4
  4. mcp_ticketer/adapters/aitrackdown.py +54 -38
  5. mcp_ticketer/adapters/github.py +175 -109
  6. mcp_ticketer/adapters/hybrid.py +90 -45
  7. mcp_ticketer/adapters/jira.py +139 -130
  8. mcp_ticketer/adapters/linear.py +374 -225
  9. mcp_ticketer/cache/__init__.py +1 -1
  10. mcp_ticketer/cache/memory.py +14 -15
  11. mcp_ticketer/cli/__init__.py +1 -1
  12. mcp_ticketer/cli/configure.py +69 -93
  13. mcp_ticketer/cli/discover.py +43 -35
  14. mcp_ticketer/cli/main.py +250 -293
  15. mcp_ticketer/cli/mcp_configure.py +39 -15
  16. mcp_ticketer/cli/migrate_config.py +10 -12
  17. mcp_ticketer/cli/queue_commands.py +21 -58
  18. mcp_ticketer/cli/utils.py +115 -60
  19. mcp_ticketer/core/__init__.py +2 -2
  20. mcp_ticketer/core/adapter.py +36 -30
  21. mcp_ticketer/core/config.py +113 -77
  22. mcp_ticketer/core/env_discovery.py +51 -19
  23. mcp_ticketer/core/http_client.py +46 -29
  24. mcp_ticketer/core/mappers.py +79 -35
  25. mcp_ticketer/core/models.py +29 -15
  26. mcp_ticketer/core/project_config.py +131 -66
  27. mcp_ticketer/core/registry.py +12 -12
  28. mcp_ticketer/mcp/__init__.py +1 -1
  29. mcp_ticketer/mcp/server.py +183 -129
  30. mcp_ticketer/queue/__init__.py +2 -2
  31. mcp_ticketer/queue/__main__.py +1 -1
  32. mcp_ticketer/queue/manager.py +29 -25
  33. mcp_ticketer/queue/queue.py +144 -82
  34. mcp_ticketer/queue/run_worker.py +2 -3
  35. mcp_ticketer/queue/worker.py +48 -33
  36. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.22.dist-info}/METADATA +1 -1
  37. mcp_ticketer-0.1.22.dist-info/RECORD +42 -0
  38. mcp_ticketer-0.1.21.dist-info/RECORD +0 -42
  39. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.22.dist-info}/WHEEL +0 -0
  40. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.22.dist-info}/entry_points.txt +0 -0
  41. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.22.dist-info}/licenses/LICENSE +0 -0
  42. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.22.dist-info}/top_level.txt +0 -0
@@ -8,19 +8,20 @@ This module provides a comprehensive configuration system that supports:
8
8
  - Hybrid mode for multi-platform synchronization
9
9
  """
10
10
 
11
- import os
12
11
  import json
13
12
  import logging
14
- from typing import Dict, Any, Optional, List
15
- from pathlib import Path
16
- from dataclasses import dataclass, field, asdict
13
+ import os
14
+ from dataclasses import asdict, dataclass, field
17
15
  from enum import Enum
16
+ from pathlib import Path
17
+ from typing import Any, Dict, List, Optional
18
18
 
19
19
  logger = logging.getLogger(__name__)
20
20
 
21
21
 
22
22
  class AdapterType(str, Enum):
23
23
  """Supported adapter types."""
24
+
24
25
  AITRACKDOWN = "aitrackdown"
25
26
  LINEAR = "linear"
26
27
  JIRA = "jira"
@@ -29,14 +30,16 @@ class AdapterType(str, Enum):
29
30
 
30
31
  class SyncStrategy(str, Enum):
31
32
  """Hybrid mode synchronization strategies."""
33
+
32
34
  PRIMARY_SOURCE = "primary_source" # One adapter is source of truth
33
- BIDIRECTIONAL = "bidirectional" # Two-way sync between adapters
34
- MIRROR = "mirror" # Clone tickets across all adapters
35
+ BIDIRECTIONAL = "bidirectional" # Two-way sync between adapters
36
+ MIRROR = "mirror" # Clone tickets across all adapters
35
37
 
36
38
 
37
39
  @dataclass
38
40
  class AdapterConfig:
39
41
  """Base configuration for a single adapter instance."""
42
+
40
43
  adapter: str
41
44
  enabled: bool = True
42
45
 
@@ -77,13 +80,25 @@ class AdapterConfig:
77
80
  return result
78
81
 
79
82
  @classmethod
80
- def from_dict(cls, data: Dict[str, Any]) -> 'AdapterConfig':
83
+ def from_dict(cls, data: Dict[str, Any]) -> "AdapterConfig":
81
84
  """Create from dictionary."""
82
85
  # Extract known fields
83
86
  known_fields = {
84
- 'adapter', 'enabled', 'api_key', 'token', 'team_id', 'team_key',
85
- 'workspace', 'server', 'email', 'api_token', 'project_key',
86
- 'owner', 'repo', 'base_path', 'project_id'
87
+ "adapter",
88
+ "enabled",
89
+ "api_key",
90
+ "token",
91
+ "team_id",
92
+ "team_key",
93
+ "workspace",
94
+ "server",
95
+ "email",
96
+ "api_token",
97
+ "project_key",
98
+ "owner",
99
+ "repo",
100
+ "base_path",
101
+ "project_id",
87
102
  }
88
103
 
89
104
  kwargs = {}
@@ -92,20 +107,21 @@ class AdapterConfig:
92
107
  for key, value in data.items():
93
108
  if key in known_fields:
94
109
  kwargs[key] = value
95
- elif key != 'additional_config':
110
+ elif key != "additional_config":
96
111
  additional[key] = value
97
112
 
98
113
  # Merge explicit additional_config
99
- if 'additional_config' in data:
100
- additional.update(data['additional_config'])
114
+ if "additional_config" in data:
115
+ additional.update(data["additional_config"])
101
116
 
102
- kwargs['additional_config'] = additional
117
+ kwargs["additional_config"] = additional
103
118
  return cls(**kwargs)
104
119
 
105
120
 
106
121
  @dataclass
107
122
  class ProjectConfig:
108
123
  """Configuration for a specific project."""
124
+
109
125
  adapter: str
110
126
  api_key: Optional[str] = None
111
127
  project_id: Optional[str] = None
@@ -117,7 +133,7 @@ class ProjectConfig:
117
133
  return {k: v for k, v in asdict(self).items() if v is not None}
118
134
 
119
135
  @classmethod
120
- def from_dict(cls, data: Dict[str, Any]) -> 'ProjectConfig':
136
+ def from_dict(cls, data: Dict[str, Any]) -> "ProjectConfig":
121
137
  """Create from dictionary."""
122
138
  return cls(**{k: v for k, v in data.items() if k in cls.__annotations__})
123
139
 
@@ -125,6 +141,7 @@ class ProjectConfig:
125
141
  @dataclass
126
142
  class HybridConfig:
127
143
  """Configuration for hybrid mode (multi-adapter sync)."""
144
+
128
145
  enabled: bool = False
129
146
  adapters: List[str] = field(default_factory=list)
130
147
  primary_adapter: Optional[str] = None
@@ -133,21 +150,22 @@ class HybridConfig:
133
150
  def to_dict(self) -> Dict[str, Any]:
134
151
  """Convert to dictionary."""
135
152
  result = asdict(self)
136
- result['sync_strategy'] = self.sync_strategy.value
153
+ result["sync_strategy"] = self.sync_strategy.value
137
154
  return result
138
155
 
139
156
  @classmethod
140
- def from_dict(cls, data: Dict[str, Any]) -> 'HybridConfig':
157
+ def from_dict(cls, data: Dict[str, Any]) -> "HybridConfig":
141
158
  """Create from dictionary."""
142
159
  data = data.copy()
143
- if 'sync_strategy' in data:
144
- data['sync_strategy'] = SyncStrategy(data['sync_strategy'])
160
+ if "sync_strategy" in data:
161
+ data["sync_strategy"] = SyncStrategy(data["sync_strategy"])
145
162
  return cls(**data)
146
163
 
147
164
 
148
165
  @dataclass
149
166
  class TicketerConfig:
150
167
  """Complete ticketer configuration with hierarchical resolution."""
168
+
151
169
  default_adapter: str = "aitrackdown"
152
170
  project_configs: Dict[str, ProjectConfig] = field(default_factory=dict)
153
171
  adapters: Dict[str, AdapterConfig] = field(default_factory=dict)
@@ -158,18 +176,16 @@ class TicketerConfig:
158
176
  return {
159
177
  "default_adapter": self.default_adapter,
160
178
  "project_configs": {
161
- path: config.to_dict()
162
- for path, config in self.project_configs.items()
179
+ path: config.to_dict() for path, config in self.project_configs.items()
163
180
  },
164
181
  "adapters": {
165
- name: config.to_dict()
166
- for name, config in self.adapters.items()
182
+ name: config.to_dict() for name, config in self.adapters.items()
167
183
  },
168
- "hybrid_mode": self.hybrid_mode.to_dict() if self.hybrid_mode else None
184
+ "hybrid_mode": self.hybrid_mode.to_dict() if self.hybrid_mode else None,
169
185
  }
170
186
 
171
187
  @classmethod
172
- def from_dict(cls, data: Dict[str, Any]) -> 'TicketerConfig':
188
+ def from_dict(cls, data: Dict[str, Any]) -> "TicketerConfig":
173
189
  """Create from dictionary."""
174
190
  # Parse project configs
175
191
  project_configs = {}
@@ -192,7 +208,7 @@ class TicketerConfig:
192
208
  default_adapter=data.get("default_adapter", "aitrackdown"),
193
209
  project_configs=project_configs,
194
210
  adapters=adapters,
195
- hybrid_mode=hybrid_mode
211
+ hybrid_mode=hybrid_mode,
196
212
  )
197
213
 
198
214
 
@@ -205,6 +221,7 @@ class ConfigValidator:
205
221
 
206
222
  Returns:
207
223
  Tuple of (is_valid, error_message)
224
+
208
225
  """
209
226
  required = ["api_key"]
210
227
  for field in required:
@@ -213,7 +230,10 @@ class ConfigValidator:
213
230
 
214
231
  # Require either team_key or team_id (team_id is preferred)
215
232
  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)"
233
+ return (
234
+ False,
235
+ "Linear config requires either team_key (short key like 'BTA') or team_id (UUID)",
236
+ )
217
237
 
218
238
  return True, None
219
239
 
@@ -223,6 +243,7 @@ class ConfigValidator:
223
243
 
224
244
  Returns:
225
245
  Tuple of (is_valid, error_message)
246
+
226
247
  """
227
248
  # token or api_key (aliases)
228
249
  has_token = config.get("token") or config.get("api_key")
@@ -251,6 +272,7 @@ class ConfigValidator:
251
272
 
252
273
  Returns:
253
274
  Tuple of (is_valid, error_message)
275
+
254
276
  """
255
277
  required = ["server", "email", "api_token"]
256
278
  for field in required:
@@ -265,18 +287,23 @@ class ConfigValidator:
265
287
  return True, None
266
288
 
267
289
  @staticmethod
268
- def validate_aitrackdown_config(config: Dict[str, Any]) -> tuple[bool, Optional[str]]:
290
+ def validate_aitrackdown_config(
291
+ config: Dict[str, Any],
292
+ ) -> tuple[bool, Optional[str]]:
269
293
  """Validate AITrackdown adapter configuration.
270
294
 
271
295
  Returns:
272
296
  Tuple of (is_valid, error_message)
297
+
273
298
  """
274
299
  # AITrackdown has minimal requirements
275
300
  # base_path is optional (defaults to .aitrackdown)
276
301
  return True, None
277
302
 
278
303
  @classmethod
279
- def validate(cls, adapter_type: str, config: Dict[str, Any]) -> tuple[bool, Optional[str]]:
304
+ def validate(
305
+ cls, adapter_type: str, config: Dict[str, Any]
306
+ ) -> tuple[bool, Optional[str]]:
280
307
  """Validate configuration for any adapter type.
281
308
 
282
309
  Args:
@@ -285,6 +312,7 @@ class ConfigValidator:
285
312
 
286
313
  Returns:
287
314
  Tuple of (is_valid, error_message)
315
+
288
316
  """
289
317
  validators = {
290
318
  AdapterType.LINEAR.value: cls.validate_linear_config,
@@ -303,47 +331,56 @@ class ConfigValidator:
303
331
  class ConfigResolver:
304
332
  """Resolve configuration from multiple sources with hierarchical precedence.
305
333
 
334
+ SECURITY: This class ONLY reads from project-local configurations
335
+ to prevent configuration leakage across projects. It will NEVER read
336
+ from user home directory or system-wide locations.
337
+
306
338
  Resolution order (highest to lowest priority):
307
339
  1. CLI overrides
308
340
  2. Environment variables
309
341
  3. Project-specific config (.mcp-ticketer/config.json)
310
342
  4. Auto-discovered .env files
311
- 5. Global config (~/.mcp-ticketer/config.json)
343
+ 5. Default to aitrackdown adapter
312
344
  """
313
345
 
314
- # Global config location
315
- GLOBAL_CONFIG_PATH = Path.home() / ".mcp-ticketer" / "config.json"
316
-
317
- # Project config location (relative to project root)
346
+ # Project config location (relative to project root) - PROJECT-LOCAL ONLY
318
347
  PROJECT_CONFIG_SUBPATH = ".mcp-ticketer" / Path("config.json")
319
348
 
320
- def __init__(self, project_path: Optional[Path] = None, enable_env_discovery: bool = True):
349
+ def __init__(
350
+ self, project_path: Optional[Path] = None, enable_env_discovery: bool = True
351
+ ):
321
352
  """Initialize config resolver.
322
353
 
323
354
  Args:
324
355
  project_path: Path to project root (defaults to cwd)
325
356
  enable_env_discovery: Enable auto-discovery from .env files (default: True)
357
+
326
358
  """
327
359
  self.project_path = project_path or Path.cwd()
328
360
  self.enable_env_discovery = enable_env_discovery
329
- self._global_config: Optional[TicketerConfig] = None
330
361
  self._project_config: Optional[TicketerConfig] = None
331
- self._discovered_config: Optional['DiscoveryResult'] = None
362
+ self._discovered_config: Optional[DiscoveryResult] = None
332
363
 
333
364
  def load_global_config(self) -> TicketerConfig:
334
- """Load global configuration from ~/.mcp-ticketer/config.json."""
335
- if self.GLOBAL_CONFIG_PATH.exists():
336
- try:
337
- with open(self.GLOBAL_CONFIG_PATH, 'r') as f:
338
- data = json.load(f)
339
- return TicketerConfig.from_dict(data)
340
- except Exception as e:
341
- logger.error(f"Failed to load global config: {e}")
365
+ """Load default configuration (global config loading removed for security).
342
366
 
343
- # Return default config
344
- return TicketerConfig()
367
+ DEPRECATED: Global config loading has been removed for security reasons.
368
+ This method now only returns default configuration.
369
+
370
+ Returns:
371
+ Default TicketerConfig with aitrackdown adapter
345
372
 
346
- def load_project_config(self, project_path: Optional[Path] = None) -> Optional[TicketerConfig]:
373
+ """
374
+ logger.info("Global config loading disabled for security, using defaults")
375
+ # Return default config with aitrackdown adapter
376
+ default_config = TicketerConfig()
377
+ if not default_config.default_adapter:
378
+ default_config.default_adapter = "aitrackdown"
379
+ return default_config
380
+
381
+ def load_project_config(
382
+ self, project_path: Optional[Path] = None
383
+ ) -> Optional[TicketerConfig]:
347
384
  """Load project-specific configuration.
348
385
 
349
386
  Args:
@@ -351,13 +388,14 @@ class ConfigResolver:
351
388
 
352
389
  Returns:
353
390
  Project config if exists, None otherwise
391
+
354
392
  """
355
393
  proj_path = project_path or self.project_path
356
394
  config_path = proj_path / self.PROJECT_CONFIG_SUBPATH
357
395
 
358
396
  if config_path.exists():
359
397
  try:
360
- with open(config_path, 'r') as f:
398
+ with open(config_path) as f:
361
399
  data = json.load(f)
362
400
  return TicketerConfig.from_dict(data)
363
401
  except Exception as e:
@@ -366,36 +404,46 @@ class ConfigResolver:
366
404
  return None
367
405
 
368
406
  def save_global_config(self, config: TicketerConfig) -> None:
369
- """Save global configuration.
407
+ """Save configuration to project-local location (global config disabled).
408
+
409
+ DEPRECATED: Global config saving has been removed for security reasons.
410
+ This method now saves to project-local config instead.
370
411
 
371
412
  Args:
372
413
  config: Configuration to save
414
+
373
415
  """
374
- self.GLOBAL_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
375
- with open(self.GLOBAL_CONFIG_PATH, 'w') as f:
376
- json.dump(config.to_dict(), f, indent=2)
377
- logger.info(f"Saved global config to {self.GLOBAL_CONFIG_PATH}")
416
+ logger.warning(
417
+ "save_global_config is deprecated and now saves to project-local config. "
418
+ "Use save_project_config instead."
419
+ )
420
+ # Save to project config instead
421
+ self.save_project_config(config)
378
422
 
379
- def save_project_config(self, config: TicketerConfig, project_path: Optional[Path] = None) -> None:
423
+ def save_project_config(
424
+ self, config: TicketerConfig, project_path: Optional[Path] = None
425
+ ) -> None:
380
426
  """Save project-specific configuration.
381
427
 
382
428
  Args:
383
429
  config: Configuration to save
384
430
  project_path: Path to project root (defaults to self.project_path)
431
+
385
432
  """
386
433
  proj_path = project_path or self.project_path
387
434
  config_path = proj_path / self.PROJECT_CONFIG_SUBPATH
388
435
 
389
436
  config_path.parent.mkdir(parents=True, exist_ok=True)
390
- with open(config_path, 'w') as f:
437
+ with open(config_path, "w") as f:
391
438
  json.dump(config.to_dict(), f, indent=2)
392
439
  logger.info(f"Saved project config to {config_path}")
393
440
 
394
- def get_discovered_config(self) -> Optional['DiscoveryResult']:
441
+ def get_discovered_config(self) -> Optional["DiscoveryResult"]:
395
442
  """Get auto-discovered configuration from .env files.
396
443
 
397
444
  Returns:
398
445
  DiscoveryResult if env discovery is enabled, None otherwise
446
+
399
447
  """
400
448
  if not self.enable_env_discovery:
401
449
  return None
@@ -403,6 +451,7 @@ class ConfigResolver:
403
451
  if self._discovered_config is None:
404
452
  # Import here to avoid circular dependency
405
453
  from .env_discovery import discover_config
454
+
406
455
  self._discovered_config = discover_config(self.project_path)
407
456
 
408
457
  return self._discovered_config
@@ -410,7 +459,7 @@ class ConfigResolver:
410
459
  def resolve_adapter_config(
411
460
  self,
412
461
  adapter_name: Optional[str] = None,
413
- cli_overrides: Optional[Dict[str, Any]] = None
462
+ cli_overrides: Optional[Dict[str, Any]] = None,
414
463
  ) -> Dict[str, Any]:
415
464
  """Resolve adapter configuration with hierarchical precedence.
416
465
 
@@ -427,6 +476,7 @@ class ConfigResolver:
427
476
 
428
477
  Returns:
429
478
  Resolved configuration dictionary
479
+
430
480
  """
431
481
  # Load configs
432
482
  global_config = self.load_global_config()
@@ -465,7 +515,8 @@ class ConfigResolver:
465
515
  if discovered_adapter:
466
516
  # Merge discovered config
467
517
  discovered_dict = {
468
- k: v for k, v in discovered_adapter.config.items()
518
+ k: v
519
+ for k, v in discovered_adapter.config.items()
469
520
  if k != "adapter" # Don't override adapter type
470
521
  }
471
522
  resolved_config.update(discovered_dict)
@@ -478,12 +529,16 @@ class ConfigResolver:
478
529
  # Check if this project has specific adapter config
479
530
  project_path_str = str(self.project_path)
480
531
  if project_path_str in project_config.project_configs:
481
- proj_adapter_config = project_config.project_configs[project_path_str].to_dict()
532
+ proj_adapter_config = project_config.project_configs[
533
+ project_path_str
534
+ ].to_dict()
482
535
  resolved_config.update(proj_adapter_config)
483
536
 
484
537
  # Also check if project has adapter-level overrides
485
538
  if target_adapter in project_config.adapters:
486
- proj_global_adapter_config = project_config.adapters[target_adapter].to_dict()
539
+ proj_global_adapter_config = project_config.adapters[
540
+ target_adapter
541
+ ].to_dict()
487
542
  resolved_config.update(proj_global_adapter_config)
488
543
 
489
544
  # 4. Apply environment variable overrides (os.getenv - HIGHER PRIORITY)
@@ -504,6 +559,7 @@ class ConfigResolver:
504
559
 
505
560
  Returns:
506
561
  Dictionary of overrides from environment
562
+
507
563
  """
508
564
  overrides = {}
509
565
 
@@ -554,9 +610,13 @@ class ConfigResolver:
554
610
 
555
611
  # Hybrid mode
556
612
  if os.getenv("MCP_TICKETER_HYBRID_MODE"):
557
- overrides["hybrid_mode_enabled"] = os.getenv("MCP_TICKETER_HYBRID_MODE").lower() == "true"
613
+ overrides["hybrid_mode_enabled"] = (
614
+ os.getenv("MCP_TICKETER_HYBRID_MODE").lower() == "true"
615
+ )
558
616
  if os.getenv("MCP_TICKETER_HYBRID_ADAPTERS"):
559
- overrides["hybrid_adapters"] = os.getenv("MCP_TICKETER_HYBRID_ADAPTERS").split(",")
617
+ overrides["hybrid_adapters"] = os.getenv(
618
+ "MCP_TICKETER_HYBRID_ADAPTERS"
619
+ ).split(",")
560
620
 
561
621
  return overrides
562
622
 
@@ -565,18 +625,22 @@ class ConfigResolver:
565
625
 
566
626
  Returns:
567
627
  HybridConfig if hybrid mode is enabled, None otherwise
628
+
568
629
  """
569
630
  # Check environment first
570
631
  if os.getenv("MCP_TICKETER_HYBRID_MODE", "").lower() == "true":
571
632
  adapters = os.getenv("MCP_TICKETER_HYBRID_ADAPTERS", "").split(",")
572
633
  return HybridConfig(
573
- enabled=True,
574
- adapters=[a.strip() for a in adapters if a.strip()]
634
+ enabled=True, adapters=[a.strip() for a in adapters if a.strip()]
575
635
  )
576
636
 
577
637
  # Check project config
578
638
  project_config = self.load_project_config()
579
- if project_config and project_config.hybrid_mode and project_config.hybrid_mode.enabled:
639
+ if (
640
+ project_config
641
+ and project_config.hybrid_mode
642
+ and project_config.hybrid_mode.enabled
643
+ ):
580
644
  return project_config.hybrid_mode
581
645
 
582
646
  # Check global config
@@ -599,6 +663,7 @@ def get_config_resolver(project_path: Optional[Path] = None) -> ConfigResolver:
599
663
 
600
664
  Returns:
601
665
  ConfigResolver instance
666
+
602
667
  """
603
668
  global _default_resolver
604
669
  if _default_resolver is None or project_path is not None:
@@ -1,6 +1,7 @@
1
1
  """Adapter registry for dynamic adapter management."""
2
2
 
3
- from typing import Dict, Type, Any, Optional
3
+ from typing import Any, Dict, Optional, Type
4
+
4
5
  from .adapter import BaseAdapter
5
6
 
6
7
 
@@ -17,6 +18,7 @@ class AdapterRegistry:
17
18
  Args:
18
19
  name: Unique name for the adapter
19
20
  adapter_class: Adapter class to register
21
+
20
22
  """
21
23
  if not issubclass(adapter_class, BaseAdapter):
22
24
  raise TypeError(f"{adapter_class} must be a subclass of BaseAdapter")
@@ -28,16 +30,14 @@ class AdapterRegistry:
28
30
 
29
31
  Args:
30
32
  name: Name of adapter to unregister
33
+
31
34
  """
32
35
  cls._adapters.pop(name, None)
33
36
  cls._instances.pop(name, None)
34
37
 
35
38
  @classmethod
36
39
  def get_adapter(
37
- cls,
38
- name: str,
39
- config: Optional[Dict[str, Any]] = None,
40
- force_new: bool = False
40
+ cls, name: str, config: Optional[Dict[str, Any]] = None, force_new: bool = False
41
41
  ) -> BaseAdapter:
42
42
  """Get or create an adapter instance.
43
43
 
@@ -53,12 +53,12 @@ class AdapterRegistry:
53
53
 
54
54
  Raises:
55
55
  ValueError: If adapter not registered
56
+
56
57
  """
57
58
  if name not in cls._adapters:
58
59
  available = ", ".join(cls._adapters.keys())
59
60
  raise ValueError(
60
- f"Adapter '{name}' not registered. "
61
- f"Available adapters: {available}"
61
+ f"Adapter '{name}' not registered. " f"Available adapters: {available}"
62
62
  )
63
63
 
64
64
  # Return cached instance if exists and not forcing new
@@ -80,6 +80,7 @@ class AdapterRegistry:
80
80
 
81
81
  Returns:
82
82
  Dictionary of adapter names to classes
83
+
83
84
  """
84
85
  return cls._adapters.copy()
85
86
 
@@ -92,6 +93,7 @@ class AdapterRegistry:
92
93
 
93
94
  Returns:
94
95
  True if registered
96
+
95
97
  """
96
98
  return name in cls._adapters
97
99
 
@@ -112,10 +114,7 @@ class AdapterRegistry:
112
114
  cls._instances.clear()
113
115
 
114
116
 
115
- def adapter_factory(
116
- adapter_type: str,
117
- config: Dict[str, Any]
118
- ) -> BaseAdapter:
117
+ def adapter_factory(adapter_type: str, config: Dict[str, Any]) -> BaseAdapter:
119
118
  """Factory function for creating adapters.
120
119
 
121
120
  Args:
@@ -124,5 +123,6 @@ def adapter_factory(
124
123
 
125
124
  Returns:
126
125
  Configured adapter instance
126
+
127
127
  """
128
- return AdapterRegistry.get_adapter(adapter_type, config)
128
+ return AdapterRegistry.get_adapter(adapter_type, config)
@@ -2,4 +2,4 @@
2
2
 
3
3
  from .server import MCPTicketServer
4
4
 
5
- __all__ = ["MCPTicketServer"]
5
+ __all__ = ["MCPTicketServer"]