mcp-ticketer 0.4.11__py3-none-any.whl → 0.12.0__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 +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +9 -3
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1308 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +334 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +313 -96
- mcp_ticketer/adapters/jira.py +251 -1
- mcp_ticketer/adapters/linear/adapter.py +524 -22
- mcp_ticketer/adapters/linear/client.py +61 -9
- mcp_ticketer/adapters/linear/mappers.py +9 -3
- mcp_ticketer/cache/memory.py +3 -3
- mcp_ticketer/cli/adapter_diagnostics.py +1 -1
- mcp_ticketer/cli/auggie_configure.py +1 -1
- mcp_ticketer/cli/codex_configure.py +80 -1
- mcp_ticketer/cli/configure.py +33 -43
- mcp_ticketer/cli/diagnostics.py +18 -16
- mcp_ticketer/cli/discover.py +288 -21
- mcp_ticketer/cli/gemini_configure.py +1 -1
- mcp_ticketer/cli/instruction_commands.py +429 -0
- mcp_ticketer/cli/linear_commands.py +99 -15
- mcp_ticketer/cli/main.py +1199 -227
- mcp_ticketer/cli/mcp_configure.py +1 -1
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +6 -6
- mcp_ticketer/cli/platform_detection.py +412 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/simple_health.py +1 -1
- mcp_ticketer/cli/ticket_commands.py +14 -13
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +45 -41
- mcp_ticketer/core/__init__.py +12 -0
- mcp_ticketer/core/adapter.py +4 -4
- mcp_ticketer/core/config.py +17 -10
- mcp_ticketer/core/env_discovery.py +33 -3
- mcp_ticketer/core/env_loader.py +7 -6
- mcp_ticketer/core/exceptions.py +3 -3
- mcp_ticketer/core/http_client.py +10 -10
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/mappers.py +1 -1
- mcp_ticketer/core/models.py +1 -1
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/project_config.py +17 -1
- mcp_ticketer/core/registry.py +1 -1
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +2 -2
- mcp_ticketer/mcp/server/__init__.py +2 -2
- mcp_ticketer/mcp/server/main.py +82 -69
- mcp_ticketer/mcp/server/tools/__init__.py +9 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +63 -16
- mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +154 -5
- mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +157 -4
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
- mcp_ticketer/queue/health_monitor.py +1 -0
- mcp_ticketer/queue/manager.py +4 -4
- mcp_ticketer/queue/queue.py +3 -3
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +2 -2
- mcp_ticketer/queue/worker.py +14 -12
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +106 -52
- mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
- mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""1Password CLI integration for secure secret management.
|
|
2
|
+
|
|
3
|
+
This module provides automatic secret loading from 1Password using the op CLI,
|
|
4
|
+
supporting:
|
|
5
|
+
- Detection of op:// secret references in .env files
|
|
6
|
+
- Automatic resolution using `op run` or `op inject`
|
|
7
|
+
- Fallback to regular .env values if 1Password CLI is not available
|
|
8
|
+
- Support for .env.1password template files
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class OnePasswordConfig:
|
|
23
|
+
"""Configuration for 1Password integration."""
|
|
24
|
+
|
|
25
|
+
enabled: bool = True
|
|
26
|
+
vault: str | None = None # Default vault for secret references
|
|
27
|
+
service_account_token: str | None = None # For CI/CD environments
|
|
28
|
+
fallback_to_env: bool = True # Fall back to regular .env if op CLI unavailable
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class OnePasswordSecretsLoader:
|
|
32
|
+
"""Load secrets from 1Password using the op CLI.
|
|
33
|
+
|
|
34
|
+
This class provides methods to:
|
|
35
|
+
1. Check if 1Password CLI is installed and authenticated
|
|
36
|
+
2. Resolve op:// secret references in .env files
|
|
37
|
+
3. Load secrets into environment variables
|
|
38
|
+
4. Create .env templates with op:// references
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, config: OnePasswordConfig | None = None) -> None:
|
|
42
|
+
"""Initialize the 1Password secrets loader.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
config: Configuration for 1Password integration
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
self.config = config or OnePasswordConfig()
|
|
49
|
+
self._op_available: bool | None = None
|
|
50
|
+
self._op_authenticated: bool | None = None
|
|
51
|
+
|
|
52
|
+
def is_op_available(self) -> bool:
|
|
53
|
+
"""Check if 1Password CLI is installed.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
True if op CLI is available, False otherwise
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
if self._op_available is None:
|
|
60
|
+
self._op_available = shutil.which("op") is not None
|
|
61
|
+
if not self._op_available:
|
|
62
|
+
logger.debug("1Password CLI (op) not found in PATH")
|
|
63
|
+
return self._op_available
|
|
64
|
+
|
|
65
|
+
def is_authenticated(self) -> bool:
|
|
66
|
+
"""Check if user is authenticated with 1Password.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
True if authenticated, False otherwise
|
|
70
|
+
|
|
71
|
+
"""
|
|
72
|
+
if not self.is_op_available():
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
if self._op_authenticated is None:
|
|
76
|
+
try:
|
|
77
|
+
# Try to list accounts to check authentication
|
|
78
|
+
result = subprocess.run(
|
|
79
|
+
["op", "account", "list"],
|
|
80
|
+
capture_output=True,
|
|
81
|
+
text=True,
|
|
82
|
+
timeout=5,
|
|
83
|
+
check=False,
|
|
84
|
+
)
|
|
85
|
+
self._op_authenticated = result.returncode == 0
|
|
86
|
+
if not self._op_authenticated:
|
|
87
|
+
logger.debug(
|
|
88
|
+
"1Password CLI not authenticated. Run 'op signin' first."
|
|
89
|
+
)
|
|
90
|
+
except (subprocess.TimeoutExpired, FileNotFoundError) as e:
|
|
91
|
+
logger.debug(f"Error checking 1Password authentication: {e}")
|
|
92
|
+
self._op_authenticated = False
|
|
93
|
+
|
|
94
|
+
return self._op_authenticated
|
|
95
|
+
|
|
96
|
+
def load_secrets_from_env_file(
|
|
97
|
+
self, env_file: Path, output_dict: dict[str, str] | None = None
|
|
98
|
+
) -> dict[str, str]:
|
|
99
|
+
"""Load secrets from .env file, resolving 1Password references.
|
|
100
|
+
|
|
101
|
+
This method:
|
|
102
|
+
1. Checks if the .env file contains op:// references
|
|
103
|
+
2. If yes and op CLI is available, uses op inject to resolve them
|
|
104
|
+
3. If no op references or CLI unavailable, returns regular dotenv values
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
env_file: Path to .env file (may contain op:// references)
|
|
108
|
+
output_dict: Optional dict to update with loaded secrets
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Dictionary of environment variables with secrets resolved
|
|
112
|
+
|
|
113
|
+
"""
|
|
114
|
+
if not env_file.exists():
|
|
115
|
+
logger.warning(f"Environment file not found: {env_file}")
|
|
116
|
+
return output_dict or {}
|
|
117
|
+
|
|
118
|
+
# Read the file to check for op:// references
|
|
119
|
+
content = env_file.read_text(encoding="utf-8")
|
|
120
|
+
has_op_references = "op://" in content
|
|
121
|
+
|
|
122
|
+
if has_op_references and self.is_authenticated():
|
|
123
|
+
# Use op inject to resolve references
|
|
124
|
+
return self._inject_secrets(env_file, output_dict)
|
|
125
|
+
else:
|
|
126
|
+
# Fall back to regular dotenv parsing
|
|
127
|
+
if has_op_references and not self.is_authenticated():
|
|
128
|
+
logger.warning(
|
|
129
|
+
f"File {env_file} contains 1Password references but op CLI "
|
|
130
|
+
"is not authenticated. Using fallback values."
|
|
131
|
+
)
|
|
132
|
+
return self._load_regular_env(env_file, output_dict)
|
|
133
|
+
|
|
134
|
+
def _inject_secrets(
|
|
135
|
+
self, env_file: Path, output_dict: dict[str, str] | None = None
|
|
136
|
+
) -> dict[str, str]:
|
|
137
|
+
"""Use op inject to resolve secret references in .env file.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
env_file: Path to .env file with op:// references
|
|
141
|
+
output_dict: Optional dict to update
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Dictionary with resolved secrets
|
|
145
|
+
|
|
146
|
+
"""
|
|
147
|
+
try:
|
|
148
|
+
# Use op inject to resolve references
|
|
149
|
+
cmd = ["op", "inject", "--in-file", str(env_file)]
|
|
150
|
+
|
|
151
|
+
# Add service account token if provided
|
|
152
|
+
env = None
|
|
153
|
+
if self.config.service_account_token:
|
|
154
|
+
env = {"OP_SERVICE_ACCOUNT_TOKEN": self.config.service_account_token}
|
|
155
|
+
|
|
156
|
+
result = subprocess.run(
|
|
157
|
+
cmd,
|
|
158
|
+
capture_output=True,
|
|
159
|
+
text=True,
|
|
160
|
+
timeout=30,
|
|
161
|
+
check=True,
|
|
162
|
+
env=env,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Parse the injected output
|
|
166
|
+
secrets = self._parse_env_output(result.stdout)
|
|
167
|
+
|
|
168
|
+
if output_dict is not None:
|
|
169
|
+
output_dict.update(secrets)
|
|
170
|
+
return output_dict
|
|
171
|
+
return secrets
|
|
172
|
+
|
|
173
|
+
except subprocess.CalledProcessError as e:
|
|
174
|
+
logger.error(f"Error injecting 1Password secrets: {e.stderr}")
|
|
175
|
+
if self.config.fallback_to_env:
|
|
176
|
+
logger.info("Falling back to regular .env parsing")
|
|
177
|
+
return self._load_regular_env(env_file, output_dict)
|
|
178
|
+
raise
|
|
179
|
+
except subprocess.TimeoutExpired:
|
|
180
|
+
logger.error("Timeout while injecting 1Password secrets")
|
|
181
|
+
if self.config.fallback_to_env:
|
|
182
|
+
return self._load_regular_env(env_file, output_dict)
|
|
183
|
+
raise
|
|
184
|
+
|
|
185
|
+
def _load_regular_env(
|
|
186
|
+
self, env_file: Path, output_dict: dict[str, str] | None = None
|
|
187
|
+
) -> dict[str, str]:
|
|
188
|
+
"""Load environment variables without 1Password resolution.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
env_file: Path to .env file
|
|
192
|
+
output_dict: Optional dict to update
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Dictionary of environment variables
|
|
196
|
+
|
|
197
|
+
"""
|
|
198
|
+
from dotenv import dotenv_values
|
|
199
|
+
|
|
200
|
+
values = dotenv_values(env_file)
|
|
201
|
+
|
|
202
|
+
if output_dict is not None:
|
|
203
|
+
output_dict.update(values)
|
|
204
|
+
return output_dict
|
|
205
|
+
return dict(values)
|
|
206
|
+
|
|
207
|
+
def _parse_env_output(self, output: str) -> dict[str, str]:
|
|
208
|
+
"""Parse environment variable output from op inject.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
output: String output from op inject
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Dictionary of parsed environment variables
|
|
215
|
+
|
|
216
|
+
"""
|
|
217
|
+
env_vars = {}
|
|
218
|
+
for line in output.splitlines():
|
|
219
|
+
line = line.strip()
|
|
220
|
+
if not line or line.startswith("#"):
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
# Split on first = only
|
|
224
|
+
if "=" in line:
|
|
225
|
+
key, value = line.split("=", 1)
|
|
226
|
+
key = key.strip()
|
|
227
|
+
value = value.strip()
|
|
228
|
+
|
|
229
|
+
# Remove quotes if present
|
|
230
|
+
if value.startswith('"') and value.endswith('"'):
|
|
231
|
+
value = value[1:-1]
|
|
232
|
+
elif value.startswith("'") and value.endswith("'"):
|
|
233
|
+
value = value[1:-1]
|
|
234
|
+
|
|
235
|
+
env_vars[key] = value
|
|
236
|
+
|
|
237
|
+
return env_vars
|
|
238
|
+
|
|
239
|
+
def create_template_file(
|
|
240
|
+
self,
|
|
241
|
+
output_path: Path,
|
|
242
|
+
adapter_type: str,
|
|
243
|
+
vault_name: str = "Development",
|
|
244
|
+
item_name: str | None = None,
|
|
245
|
+
) -> None:
|
|
246
|
+
"""Create a .env template file with 1Password secret references.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
output_path: Path where to create the template file
|
|
250
|
+
adapter_type: Type of adapter (linear, github, jira, aitrackdown)
|
|
251
|
+
vault_name: Name of 1Password vault to use
|
|
252
|
+
item_name: Name of 1Password item (defaults to adapter name)
|
|
253
|
+
|
|
254
|
+
"""
|
|
255
|
+
if item_name is None:
|
|
256
|
+
item_name = adapter_type.upper()
|
|
257
|
+
|
|
258
|
+
templates = {
|
|
259
|
+
"linear": f"""# Linear Configuration with 1Password
|
|
260
|
+
# This file contains secret references that will be resolved by 1Password CLI
|
|
261
|
+
# Run: op run --env-file=.env.1password -- mcp-ticketer discover
|
|
262
|
+
|
|
263
|
+
LINEAR_API_KEY="op://{vault_name}/{item_name}/api_key"
|
|
264
|
+
LINEAR_TEAM_ID="op://{vault_name}/{item_name}/team_id"
|
|
265
|
+
LINEAR_TEAM_KEY="op://{vault_name}/{item_name}/team_key"
|
|
266
|
+
LINEAR_PROJECT_ID="op://{vault_name}/{item_name}/project_id"
|
|
267
|
+
""",
|
|
268
|
+
"github": f"""# GitHub Configuration with 1Password
|
|
269
|
+
# This file contains secret references that will be resolved by 1Password CLI
|
|
270
|
+
# Run: op run --env-file=.env.1password -- mcp-ticketer discover
|
|
271
|
+
|
|
272
|
+
GITHUB_TOKEN="op://{vault_name}/{item_name}/token"
|
|
273
|
+
GITHUB_OWNER="op://{vault_name}/{item_name}/owner"
|
|
274
|
+
GITHUB_REPO="op://{vault_name}/{item_name}/repo"
|
|
275
|
+
""",
|
|
276
|
+
"jira": f"""# JIRA Configuration with 1Password
|
|
277
|
+
# This file contains secret references that will be resolved by 1Password CLI
|
|
278
|
+
# Run: op run --env-file=.env.1password -- mcp-ticketer discover
|
|
279
|
+
|
|
280
|
+
JIRA_SERVER="op://{vault_name}/{item_name}/server"
|
|
281
|
+
JIRA_EMAIL="op://{vault_name}/{item_name}/email"
|
|
282
|
+
JIRA_API_TOKEN="op://{vault_name}/{item_name}/api_token"
|
|
283
|
+
JIRA_PROJECT_KEY="op://{vault_name}/{item_name}/project_key"
|
|
284
|
+
""",
|
|
285
|
+
"aitrackdown": """# AITrackdown Configuration
|
|
286
|
+
# AITrackdown doesn't use API keys, but you can store the base path
|
|
287
|
+
|
|
288
|
+
AITRACKDOWN_PATH=".aitrackdown"
|
|
289
|
+
""",
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
template = templates.get(adapter_type.lower(), "")
|
|
293
|
+
if template:
|
|
294
|
+
output_path.write_text(template, encoding="utf-8")
|
|
295
|
+
logger.info(f"Created 1Password template file: {output_path}")
|
|
296
|
+
else:
|
|
297
|
+
logger.error(f"Unknown adapter type: {adapter_type}")
|
|
298
|
+
|
|
299
|
+
def run_with_secrets(
|
|
300
|
+
self, command: list[str], env_file: Path | None = None
|
|
301
|
+
) -> subprocess.CompletedProcess[str]:
|
|
302
|
+
"""Run a command with secrets loaded from 1Password.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
command: Command and arguments to run
|
|
306
|
+
env_file: Optional .env file with secret references
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
CompletedProcess result
|
|
310
|
+
|
|
311
|
+
"""
|
|
312
|
+
if not self.is_authenticated():
|
|
313
|
+
raise RuntimeError(
|
|
314
|
+
"1Password CLI not authenticated. Run 'op signin' first."
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
cmd = ["op", "run"]
|
|
318
|
+
|
|
319
|
+
if env_file:
|
|
320
|
+
cmd.extend(["--env-file", str(env_file)])
|
|
321
|
+
|
|
322
|
+
cmd.append("--")
|
|
323
|
+
cmd.extend(command)
|
|
324
|
+
|
|
325
|
+
return subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def check_op_cli_status() -> dict[str, Any]:
|
|
329
|
+
"""Check the status of 1Password CLI installation and authentication.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Dictionary with status information
|
|
333
|
+
|
|
334
|
+
"""
|
|
335
|
+
loader = OnePasswordSecretsLoader()
|
|
336
|
+
|
|
337
|
+
status = {
|
|
338
|
+
"installed": loader.is_op_available(),
|
|
339
|
+
"authenticated": False,
|
|
340
|
+
"version": None,
|
|
341
|
+
"accounts": [],
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if status["installed"]:
|
|
345
|
+
# Get version
|
|
346
|
+
try:
|
|
347
|
+
result = subprocess.run(
|
|
348
|
+
["op", "--version"],
|
|
349
|
+
capture_output=True,
|
|
350
|
+
text=True,
|
|
351
|
+
timeout=5,
|
|
352
|
+
check=False,
|
|
353
|
+
)
|
|
354
|
+
if result.returncode == 0:
|
|
355
|
+
status["version"] = result.stdout.strip()
|
|
356
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
357
|
+
pass
|
|
358
|
+
|
|
359
|
+
# Check authentication
|
|
360
|
+
status["authenticated"] = loader.is_authenticated()
|
|
361
|
+
|
|
362
|
+
# Get accounts if authenticated
|
|
363
|
+
if status["authenticated"]:
|
|
364
|
+
try:
|
|
365
|
+
result = subprocess.run(
|
|
366
|
+
["op", "account", "list", "--format=json"],
|
|
367
|
+
capture_output=True,
|
|
368
|
+
text=True,
|
|
369
|
+
timeout=5,
|
|
370
|
+
check=False,
|
|
371
|
+
)
|
|
372
|
+
if result.returncode == 0:
|
|
373
|
+
import json
|
|
374
|
+
|
|
375
|
+
status["accounts"] = json.loads(result.stdout)
|
|
376
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, json.JSONDecodeError):
|
|
377
|
+
pass
|
|
378
|
+
|
|
379
|
+
return status
|
|
@@ -174,9 +174,14 @@ class TicketerConfig:
|
|
|
174
174
|
adapters: dict[str, AdapterConfig] = field(default_factory=dict)
|
|
175
175
|
hybrid_mode: HybridConfig | None = None
|
|
176
176
|
|
|
177
|
+
# Default values for ticket operations
|
|
178
|
+
default_user: str | None = None # Default assignee (user_id or email)
|
|
179
|
+
default_project: str | None = None # Default project/epic ID
|
|
180
|
+
default_epic: str | None = None # Alias for default_project (backward compat)
|
|
181
|
+
|
|
177
182
|
def to_dict(self) -> dict[str, Any]:
|
|
178
183
|
"""Convert to dictionary for JSON serialization."""
|
|
179
|
-
|
|
184
|
+
result = {
|
|
180
185
|
"default_adapter": self.default_adapter,
|
|
181
186
|
"project_configs": {
|
|
182
187
|
path: config.to_dict() for path, config in self.project_configs.items()
|
|
@@ -186,6 +191,14 @@ class TicketerConfig:
|
|
|
186
191
|
},
|
|
187
192
|
"hybrid_mode": self.hybrid_mode.to_dict() if self.hybrid_mode else None,
|
|
188
193
|
}
|
|
194
|
+
# Add optional fields if set
|
|
195
|
+
if self.default_user is not None:
|
|
196
|
+
result["default_user"] = self.default_user
|
|
197
|
+
if self.default_project is not None:
|
|
198
|
+
result["default_project"] = self.default_project
|
|
199
|
+
if self.default_epic is not None:
|
|
200
|
+
result["default_epic"] = self.default_epic
|
|
201
|
+
return result
|
|
189
202
|
|
|
190
203
|
@classmethod
|
|
191
204
|
def from_dict(cls, data: dict[str, Any]) -> "TicketerConfig":
|
|
@@ -212,6 +225,9 @@ class TicketerConfig:
|
|
|
212
225
|
project_configs=project_configs,
|
|
213
226
|
adapters=adapters,
|
|
214
227
|
hybrid_mode=hybrid_mode,
|
|
228
|
+
default_user=data.get("default_user"),
|
|
229
|
+
default_project=data.get("default_project"),
|
|
230
|
+
default_epic=data.get("default_epic"),
|
|
215
231
|
)
|
|
216
232
|
|
|
217
233
|
|
mcp_ticketer/core/registry.py
CHANGED
|
@@ -115,7 +115,7 @@ class AdapterRegistry:
|
|
|
115
115
|
|
|
116
116
|
|
|
117
117
|
def adapter_factory(adapter_type: str, config: dict[str, Any]) -> BaseAdapter:
|
|
118
|
-
"""
|
|
118
|
+
"""Create adapter instance using factory pattern.
|
|
119
119
|
|
|
120
120
|
Args:
|
|
121
121
|
adapter_type: Type of adapter to create
|