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,381 @@
|
|
|
1
|
+
"""Configuration management tools for MCP ticketer.
|
|
2
|
+
|
|
3
|
+
This module provides tools for managing project-local configuration including
|
|
4
|
+
default adapter, project, and user settings. All configuration is stored in
|
|
5
|
+
.mcp-ticketer/config.json within the project root.
|
|
6
|
+
|
|
7
|
+
Design Decision: Project-Local Configuration Only
|
|
8
|
+
-------------------------------------------------
|
|
9
|
+
For security and isolation, this module ONLY manages project-local configuration
|
|
10
|
+
stored in .mcp-ticketer/config.json. It never reads from or writes to user home
|
|
11
|
+
directory or system-wide locations to prevent configuration leakage across projects.
|
|
12
|
+
|
|
13
|
+
Configuration stored:
|
|
14
|
+
- default_adapter: Primary adapter to use for ticket operations
|
|
15
|
+
- default_project: Default epic/project ID for new tickets
|
|
16
|
+
- default_user: Default assignee for new tickets (user_id or email)
|
|
17
|
+
- default_epic: Alias for default_project (backward compatibility)
|
|
18
|
+
|
|
19
|
+
Error Handling:
|
|
20
|
+
- All tools validate input before modifying configuration
|
|
21
|
+
- Adapter names are validated against AdapterRegistry
|
|
22
|
+
- Configuration file is created atomically to prevent corruption
|
|
23
|
+
- Detailed error messages for invalid configurations
|
|
24
|
+
|
|
25
|
+
Performance: Configuration is cached in memory by ConfigResolver,
|
|
26
|
+
so repeated reads are fast (O(1) after first load).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
from ....core.project_config import AdapterType, ConfigResolver, TicketerConfig
|
|
33
|
+
from ..server_sdk import mcp
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_resolver() -> ConfigResolver:
|
|
37
|
+
"""Get or create the configuration resolver.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
ConfigResolver instance for current working directory
|
|
41
|
+
|
|
42
|
+
Design Decision: Uses CWD as project root, assuming MCP server
|
|
43
|
+
is started from project directory. This matches user expectations
|
|
44
|
+
and aligns with how other development tools operate.
|
|
45
|
+
|
|
46
|
+
Note: Creates a new resolver each time to avoid caching issues
|
|
47
|
+
in tests and ensure current working directory is always used.
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
return ConfigResolver(project_path=Path.cwd())
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@mcp.tool()
|
|
54
|
+
async def config_set_primary_adapter(adapter: str) -> dict[str, Any]:
|
|
55
|
+
"""Set the default adapter for ticket operations.
|
|
56
|
+
|
|
57
|
+
Updates the project-local configuration (.mcp-ticketer/config.json)
|
|
58
|
+
to use the specified adapter as the default for all ticket operations.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
adapter: Adapter name to set as primary. Must be one of:
|
|
62
|
+
- "aitrackdown" (file-based tracking)
|
|
63
|
+
- "linear" (Linear.app)
|
|
64
|
+
- "github" (GitHub Issues)
|
|
65
|
+
- "jira" (Atlassian JIRA)
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Dictionary containing:
|
|
69
|
+
- status: "completed" or "error"
|
|
70
|
+
- message: Success or error message
|
|
71
|
+
- previous_adapter: Previous default adapter (if successful)
|
|
72
|
+
- new_adapter: New default adapter (if successful)
|
|
73
|
+
- error: Error details (if failed)
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
>>> result = await config_set_primary_adapter("linear")
|
|
77
|
+
>>> print(result)
|
|
78
|
+
{
|
|
79
|
+
"status": "completed",
|
|
80
|
+
"message": "Default adapter set to 'linear'",
|
|
81
|
+
"previous_adapter": "aitrackdown",
|
|
82
|
+
"new_adapter": "linear"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
Error Conditions:
|
|
86
|
+
- Invalid adapter name: Returns error with valid options
|
|
87
|
+
- Configuration file write failure: Returns error with file path
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
# Validate adapter name against registry
|
|
92
|
+
valid_adapters = [adapter_type.value for adapter_type in AdapterType]
|
|
93
|
+
if adapter.lower() not in valid_adapters:
|
|
94
|
+
return {
|
|
95
|
+
"status": "error",
|
|
96
|
+
"error": f"Invalid adapter '{adapter}'. Must be one of: {', '.join(valid_adapters)}",
|
|
97
|
+
"valid_adapters": valid_adapters,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Load current configuration
|
|
101
|
+
resolver = get_resolver()
|
|
102
|
+
config = resolver.load_project_config() or TicketerConfig()
|
|
103
|
+
|
|
104
|
+
# Store previous adapter for response
|
|
105
|
+
previous_adapter = config.default_adapter
|
|
106
|
+
|
|
107
|
+
# Update default adapter
|
|
108
|
+
config.default_adapter = adapter.lower()
|
|
109
|
+
|
|
110
|
+
# Save configuration
|
|
111
|
+
resolver.save_project_config(config)
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
"status": "completed",
|
|
115
|
+
"message": f"Default adapter set to '{adapter.lower()}'",
|
|
116
|
+
"previous_adapter": previous_adapter,
|
|
117
|
+
"new_adapter": adapter.lower(),
|
|
118
|
+
"config_path": str(resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH),
|
|
119
|
+
}
|
|
120
|
+
except Exception as e:
|
|
121
|
+
return {
|
|
122
|
+
"status": "error",
|
|
123
|
+
"error": f"Failed to set default adapter: {str(e)}",
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@mcp.tool()
|
|
128
|
+
async def config_set_default_project(
|
|
129
|
+
project_id: str,
|
|
130
|
+
project_key: str | None = None,
|
|
131
|
+
) -> dict[str, Any]:
|
|
132
|
+
"""Set the default project/epic for new tickets.
|
|
133
|
+
|
|
134
|
+
Updates the project-local configuration to automatically assign new tickets
|
|
135
|
+
to the specified project or epic. This is useful for teams working primarily
|
|
136
|
+
on a single project or feature area.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
project_id: Project or epic ID to set as default (required)
|
|
140
|
+
project_key: Optional project key (for adapters that use keys vs IDs)
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Dictionary containing:
|
|
144
|
+
- status: "completed" or "error"
|
|
145
|
+
- message: Success or error message
|
|
146
|
+
- previous_project: Previous default project (if any)
|
|
147
|
+
- new_project: New default project ID
|
|
148
|
+
- error: Error details (if failed)
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
>>> result = await config_set_default_project("PROJ-123")
|
|
152
|
+
>>> print(result)
|
|
153
|
+
{
|
|
154
|
+
"status": "completed",
|
|
155
|
+
"message": "Default project set to 'PROJ-123'",
|
|
156
|
+
"previous_project": None,
|
|
157
|
+
"new_project": "PROJ-123"
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
Usage Notes:
|
|
161
|
+
- This sets both default_project and default_epic (for backward compatibility)
|
|
162
|
+
- Empty string or null clears the default project
|
|
163
|
+
- Project ID is not validated (allows flexibility across adapters)
|
|
164
|
+
|
|
165
|
+
"""
|
|
166
|
+
try:
|
|
167
|
+
# Load current configuration
|
|
168
|
+
resolver = get_resolver()
|
|
169
|
+
config = resolver.load_project_config() or TicketerConfig()
|
|
170
|
+
|
|
171
|
+
# Store previous project for response
|
|
172
|
+
previous_project = config.default_project or config.default_epic
|
|
173
|
+
|
|
174
|
+
# Update default project (and epic for backward compat)
|
|
175
|
+
config.default_project = project_id if project_id else None
|
|
176
|
+
config.default_epic = project_id if project_id else None
|
|
177
|
+
|
|
178
|
+
# Save configuration
|
|
179
|
+
resolver.save_project_config(config)
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
"status": "completed",
|
|
183
|
+
"message": (
|
|
184
|
+
f"Default project set to '{project_id}'"
|
|
185
|
+
if project_id
|
|
186
|
+
else "Default project cleared"
|
|
187
|
+
),
|
|
188
|
+
"previous_project": previous_project,
|
|
189
|
+
"new_project": project_id,
|
|
190
|
+
"config_path": str(resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH),
|
|
191
|
+
}
|
|
192
|
+
except Exception as e:
|
|
193
|
+
return {
|
|
194
|
+
"status": "error",
|
|
195
|
+
"error": f"Failed to set default project: {str(e)}",
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@mcp.tool()
|
|
200
|
+
async def config_set_default_user(
|
|
201
|
+
user_id: str,
|
|
202
|
+
user_email: str | None = None,
|
|
203
|
+
) -> dict[str, Any]:
|
|
204
|
+
"""Set the default assignee for new tickets.
|
|
205
|
+
|
|
206
|
+
Updates the project-local configuration to automatically assign new tickets
|
|
207
|
+
to the specified user. Supports both user IDs and email addresses depending
|
|
208
|
+
on adapter requirements.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
user_id: User identifier or email to set as default assignee (required)
|
|
212
|
+
user_email: Optional email (for adapters that require separate email field)
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Dictionary containing:
|
|
216
|
+
- status: "completed" or "error"
|
|
217
|
+
- message: Success or error message
|
|
218
|
+
- previous_user: Previous default user (if any)
|
|
219
|
+
- new_user: New default user ID
|
|
220
|
+
- error: Error details (if failed)
|
|
221
|
+
|
|
222
|
+
Example:
|
|
223
|
+
>>> result = await config_set_default_user("user123")
|
|
224
|
+
>>> print(result)
|
|
225
|
+
{
|
|
226
|
+
"status": "completed",
|
|
227
|
+
"message": "Default user set to 'user123'",
|
|
228
|
+
"previous_user": None,
|
|
229
|
+
"new_user": "user123"
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
Example with email:
|
|
233
|
+
>>> result = await config_set_default_user("user@example.com")
|
|
234
|
+
>>> print(result)
|
|
235
|
+
{
|
|
236
|
+
"status": "completed",
|
|
237
|
+
"message": "Default user set to 'user@example.com'",
|
|
238
|
+
"previous_user": "old_user@example.com",
|
|
239
|
+
"new_user": "user@example.com"
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
Usage Notes:
|
|
243
|
+
- User ID/email is not validated (allows flexibility across adapters)
|
|
244
|
+
- Empty string or null clears the default user
|
|
245
|
+
- Some adapters prefer email, others prefer user UUID
|
|
246
|
+
|
|
247
|
+
"""
|
|
248
|
+
try:
|
|
249
|
+
# Load current configuration
|
|
250
|
+
resolver = get_resolver()
|
|
251
|
+
config = resolver.load_project_config() or TicketerConfig()
|
|
252
|
+
|
|
253
|
+
# Store previous user for response
|
|
254
|
+
previous_user = config.default_user
|
|
255
|
+
|
|
256
|
+
# Update default user
|
|
257
|
+
config.default_user = user_id if user_id else None
|
|
258
|
+
|
|
259
|
+
# Save configuration
|
|
260
|
+
resolver.save_project_config(config)
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
"status": "completed",
|
|
264
|
+
"message": (
|
|
265
|
+
f"Default user set to '{user_id}'"
|
|
266
|
+
if user_id
|
|
267
|
+
else "Default user cleared"
|
|
268
|
+
),
|
|
269
|
+
"previous_user": previous_user,
|
|
270
|
+
"new_user": user_id,
|
|
271
|
+
"config_path": str(resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH),
|
|
272
|
+
}
|
|
273
|
+
except Exception as e:
|
|
274
|
+
return {
|
|
275
|
+
"status": "error",
|
|
276
|
+
"error": f"Failed to set default user: {str(e)}",
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@mcp.tool()
|
|
281
|
+
async def config_get() -> dict[str, Any]:
|
|
282
|
+
"""Get current configuration settings.
|
|
283
|
+
|
|
284
|
+
Retrieves the current project-local configuration including default adapter,
|
|
285
|
+
project, user, and all adapter-specific settings.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Dictionary containing:
|
|
289
|
+
- status: "completed" or "error"
|
|
290
|
+
- config: Complete configuration dictionary including:
|
|
291
|
+
- default_adapter: Primary adapter name
|
|
292
|
+
- default_project: Default project/epic ID (if set)
|
|
293
|
+
- default_user: Default assignee (if set)
|
|
294
|
+
- adapters: All adapter configurations
|
|
295
|
+
- hybrid_mode: Hybrid mode settings (if enabled)
|
|
296
|
+
- config_path: Path to configuration file
|
|
297
|
+
- error: Error details (if failed)
|
|
298
|
+
|
|
299
|
+
Example:
|
|
300
|
+
>>> result = await config_get()
|
|
301
|
+
>>> print(result)
|
|
302
|
+
{
|
|
303
|
+
"status": "completed",
|
|
304
|
+
"config": {
|
|
305
|
+
"default_adapter": "linear",
|
|
306
|
+
"default_project": "PROJ-123",
|
|
307
|
+
"default_user": "user@example.com",
|
|
308
|
+
"adapters": {
|
|
309
|
+
"linear": {"api_key": "***", "team_id": "..."}
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
"config_path": "/project/.mcp-ticketer/config.json"
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
Usage Notes:
|
|
316
|
+
- Sensitive values (API keys) are masked in the response
|
|
317
|
+
- Returns default values if no configuration file exists
|
|
318
|
+
- Configuration is merged from multiple sources (env vars, .env files, config.json)
|
|
319
|
+
|
|
320
|
+
"""
|
|
321
|
+
try:
|
|
322
|
+
# Load current configuration
|
|
323
|
+
resolver = get_resolver()
|
|
324
|
+
config = resolver.load_project_config() or TicketerConfig()
|
|
325
|
+
|
|
326
|
+
# Convert to dictionary
|
|
327
|
+
config_dict = config.to_dict()
|
|
328
|
+
|
|
329
|
+
# Mask sensitive values (API keys, tokens)
|
|
330
|
+
masked_config = _mask_sensitive_values(config_dict)
|
|
331
|
+
|
|
332
|
+
config_path = resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH
|
|
333
|
+
config_exists = config_path.exists()
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
"status": "completed",
|
|
337
|
+
"config": masked_config,
|
|
338
|
+
"config_path": str(config_path),
|
|
339
|
+
"config_exists": config_exists,
|
|
340
|
+
"message": (
|
|
341
|
+
"Configuration retrieved successfully"
|
|
342
|
+
if config_exists
|
|
343
|
+
else "No configuration file found, showing defaults"
|
|
344
|
+
),
|
|
345
|
+
}
|
|
346
|
+
except Exception as e:
|
|
347
|
+
return {
|
|
348
|
+
"status": "error",
|
|
349
|
+
"error": f"Failed to retrieve configuration: {str(e)}",
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _mask_sensitive_values(config: dict[str, Any]) -> dict[str, Any]:
|
|
354
|
+
"""Mask sensitive values in configuration dictionary.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
config: Configuration dictionary
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
Configuration dictionary with sensitive values masked
|
|
361
|
+
|
|
362
|
+
Implementation Details:
|
|
363
|
+
- Recursively processes nested dictionaries
|
|
364
|
+
- Masks any field containing: key, token, password, secret
|
|
365
|
+
- Preserves structure for debugging while protecting credentials
|
|
366
|
+
|
|
367
|
+
"""
|
|
368
|
+
masked = {}
|
|
369
|
+
sensitive_keys = {"api_key", "token", "password", "secret", "api_token"}
|
|
370
|
+
|
|
371
|
+
for key, value in config.items():
|
|
372
|
+
if isinstance(value, dict):
|
|
373
|
+
# Recursively mask nested dictionaries
|
|
374
|
+
masked[key] = _mask_sensitive_values(value)
|
|
375
|
+
elif any(sensitive in key.lower() for sensitive in sensitive_keys):
|
|
376
|
+
# Mask sensitive values
|
|
377
|
+
masked[key] = "***" if value else None
|
|
378
|
+
else:
|
|
379
|
+
masked[key] = value
|
|
380
|
+
|
|
381
|
+
return masked
|
|
@@ -7,10 +7,13 @@ This module implements tools for managing the three-level ticket hierarchy:
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
10
11
|
from typing import Any
|
|
11
12
|
|
|
12
13
|
from ....core.models import Epic, Priority, Task, TicketType
|
|
14
|
+
from ....core.project_config import ConfigResolver, TicketerConfig
|
|
13
15
|
from ..server_sdk import get_adapter, mcp
|
|
16
|
+
from .ticket_tools import detect_and_apply_labels
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
@mcp.tool()
|
|
@@ -159,8 +162,13 @@ async def issue_create(
|
|
|
159
162
|
epic_id: str | None = None,
|
|
160
163
|
assignee: str | None = None,
|
|
161
164
|
priority: str = "medium",
|
|
165
|
+
tags: list[str] | None = None,
|
|
166
|
+
auto_detect_labels: bool = True,
|
|
162
167
|
) -> dict[str, Any]:
|
|
163
|
-
"""Create a new issue (standard work item).
|
|
168
|
+
"""Create a new issue (standard work item) with automatic label detection.
|
|
169
|
+
|
|
170
|
+
This tool automatically scans available labels/tags and intelligently
|
|
171
|
+
applies relevant ones based on the issue title and description.
|
|
164
172
|
|
|
165
173
|
Args:
|
|
166
174
|
title: Issue title (required)
|
|
@@ -168,6 +176,8 @@ async def issue_create(
|
|
|
168
176
|
epic_id: Parent epic ID to link this issue to
|
|
169
177
|
assignee: User ID or email to assign the issue to
|
|
170
178
|
priority: Priority level - must be one of: low, medium, high, critical
|
|
179
|
+
tags: List of tags to categorize the issue (auto-detection adds to these)
|
|
180
|
+
auto_detect_labels: Automatically detect and apply relevant labels (default: True)
|
|
171
181
|
|
|
172
182
|
Returns:
|
|
173
183
|
Created issue details including ID and metadata, or error information
|
|
@@ -185,14 +195,41 @@ async def issue_create(
|
|
|
185
195
|
"error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
|
|
186
196
|
}
|
|
187
197
|
|
|
198
|
+
# Use default_user if no assignee specified
|
|
199
|
+
final_assignee = assignee
|
|
200
|
+
if final_assignee is None:
|
|
201
|
+
resolver = ConfigResolver(project_path=Path.cwd())
|
|
202
|
+
config = resolver.load_project_config() or TicketerConfig()
|
|
203
|
+
if config.default_user:
|
|
204
|
+
final_assignee = config.default_user
|
|
205
|
+
|
|
206
|
+
# Use default_project if no epic_id specified
|
|
207
|
+
final_epic_id = epic_id
|
|
208
|
+
if final_epic_id is None:
|
|
209
|
+
resolver = ConfigResolver(project_path=Path.cwd())
|
|
210
|
+
config = resolver.load_project_config() or TicketerConfig()
|
|
211
|
+
# Try default_project first, fall back to default_epic
|
|
212
|
+
if config.default_project:
|
|
213
|
+
final_epic_id = config.default_project
|
|
214
|
+
elif config.default_epic:
|
|
215
|
+
final_epic_id = config.default_epic
|
|
216
|
+
|
|
217
|
+
# Auto-detect labels if enabled
|
|
218
|
+
final_tags = tags
|
|
219
|
+
if auto_detect_labels:
|
|
220
|
+
final_tags = await detect_and_apply_labels(
|
|
221
|
+
adapter, title, description or "", tags
|
|
222
|
+
)
|
|
223
|
+
|
|
188
224
|
# Create issue (Task with ISSUE type)
|
|
189
225
|
issue = Task(
|
|
190
226
|
title=title,
|
|
191
227
|
description=description or "",
|
|
192
228
|
ticket_type=TicketType.ISSUE,
|
|
193
|
-
parent_epic=
|
|
194
|
-
assignee=
|
|
229
|
+
parent_epic=final_epic_id,
|
|
230
|
+
assignee=final_assignee,
|
|
195
231
|
priority=priority_enum,
|
|
232
|
+
tags=final_tags or [],
|
|
196
233
|
)
|
|
197
234
|
|
|
198
235
|
# Create via adapter
|
|
@@ -201,6 +238,8 @@ async def issue_create(
|
|
|
201
238
|
return {
|
|
202
239
|
"status": "completed",
|
|
203
240
|
"issue": created.model_dump(),
|
|
241
|
+
"labels_applied": created.tags or [],
|
|
242
|
+
"auto_detected": auto_detect_labels,
|
|
204
243
|
}
|
|
205
244
|
except Exception as e:
|
|
206
245
|
return {
|
|
@@ -261,8 +300,13 @@ async def task_create(
|
|
|
261
300
|
issue_id: str | None = None,
|
|
262
301
|
assignee: str | None = None,
|
|
263
302
|
priority: str = "medium",
|
|
303
|
+
tags: list[str] | None = None,
|
|
304
|
+
auto_detect_labels: bool = True,
|
|
264
305
|
) -> dict[str, Any]:
|
|
265
|
-
"""Create a new task (sub-work item).
|
|
306
|
+
"""Create a new task (sub-work item) with automatic label detection.
|
|
307
|
+
|
|
308
|
+
This tool automatically scans available labels/tags and intelligently
|
|
309
|
+
applies relevant ones based on the task title and description.
|
|
266
310
|
|
|
267
311
|
Args:
|
|
268
312
|
title: Task title (required)
|
|
@@ -270,6 +314,8 @@ async def task_create(
|
|
|
270
314
|
issue_id: Parent issue ID to link this task to
|
|
271
315
|
assignee: User ID or email to assign the task to
|
|
272
316
|
priority: Priority level - must be one of: low, medium, high, critical
|
|
317
|
+
tags: List of tags to categorize the task (auto-detection adds to these)
|
|
318
|
+
auto_detect_labels: Automatically detect and apply relevant labels (default: True)
|
|
273
319
|
|
|
274
320
|
Returns:
|
|
275
321
|
Created task details including ID and metadata, or error information
|
|
@@ -287,14 +333,30 @@ async def task_create(
|
|
|
287
333
|
"error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
|
|
288
334
|
}
|
|
289
335
|
|
|
336
|
+
# Use default_user if no assignee specified
|
|
337
|
+
final_assignee = assignee
|
|
338
|
+
if final_assignee is None:
|
|
339
|
+
resolver = ConfigResolver(project_path=Path.cwd())
|
|
340
|
+
config = resolver.load_project_config() or TicketerConfig()
|
|
341
|
+
if config.default_user:
|
|
342
|
+
final_assignee = config.default_user
|
|
343
|
+
|
|
344
|
+
# Auto-detect labels if enabled
|
|
345
|
+
final_tags = tags
|
|
346
|
+
if auto_detect_labels:
|
|
347
|
+
final_tags = await detect_and_apply_labels(
|
|
348
|
+
adapter, title, description or "", tags
|
|
349
|
+
)
|
|
350
|
+
|
|
290
351
|
# Create task (Task with TASK type)
|
|
291
352
|
task = Task(
|
|
292
353
|
title=title,
|
|
293
354
|
description=description or "",
|
|
294
355
|
ticket_type=TicketType.TASK,
|
|
295
356
|
parent_issue=issue_id,
|
|
296
|
-
assignee=
|
|
357
|
+
assignee=final_assignee,
|
|
297
358
|
priority=priority_enum,
|
|
359
|
+
tags=final_tags or [],
|
|
298
360
|
)
|
|
299
361
|
|
|
300
362
|
# Create via adapter
|
|
@@ -303,6 +365,8 @@ async def task_create(
|
|
|
303
365
|
return {
|
|
304
366
|
"status": "completed",
|
|
305
367
|
"task": created.model_dump(),
|
|
368
|
+
"labels_applied": created.tags or [],
|
|
369
|
+
"auto_detected": auto_detect_labels,
|
|
306
370
|
}
|
|
307
371
|
except Exception as e:
|
|
308
372
|
return {
|
|
@@ -311,6 +375,91 @@ async def task_create(
|
|
|
311
375
|
}
|
|
312
376
|
|
|
313
377
|
|
|
378
|
+
@mcp.tool()
|
|
379
|
+
async def epic_update(
|
|
380
|
+
epic_id: str,
|
|
381
|
+
title: str | None = None,
|
|
382
|
+
description: str | None = None,
|
|
383
|
+
state: str | None = None,
|
|
384
|
+
target_date: str | None = None,
|
|
385
|
+
) -> dict[str, Any]:
|
|
386
|
+
"""Update an existing epic's metadata and description.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
epic_id: Epic identifier (required)
|
|
390
|
+
title: New title for the epic
|
|
391
|
+
description: New description for the epic
|
|
392
|
+
state: New state (open, in_progress, done, closed)
|
|
393
|
+
target_date: Target completion date in ISO format (YYYY-MM-DD)
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
Updated epic details, or error information
|
|
397
|
+
|
|
398
|
+
"""
|
|
399
|
+
try:
|
|
400
|
+
adapter = get_adapter()
|
|
401
|
+
|
|
402
|
+
# Check if adapter supports epic updates
|
|
403
|
+
if not hasattr(adapter, "update_epic"):
|
|
404
|
+
return {
|
|
405
|
+
"status": "error",
|
|
406
|
+
"error": f"Epic updates not supported by {type(adapter).__name__} adapter",
|
|
407
|
+
"epic_id": epic_id,
|
|
408
|
+
"note": "Use ticket_update instead for basic field updates",
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
# Build updates dictionary
|
|
412
|
+
updates = {}
|
|
413
|
+
if title is not None:
|
|
414
|
+
updates["title"] = title
|
|
415
|
+
if description is not None:
|
|
416
|
+
updates["description"] = description
|
|
417
|
+
if state is not None:
|
|
418
|
+
updates["state"] = state
|
|
419
|
+
if target_date is not None:
|
|
420
|
+
# Parse target date if provided
|
|
421
|
+
try:
|
|
422
|
+
target_datetime = datetime.fromisoformat(target_date)
|
|
423
|
+
updates["target_date"] = target_datetime
|
|
424
|
+
except ValueError:
|
|
425
|
+
return {
|
|
426
|
+
"status": "error",
|
|
427
|
+
"error": f"Invalid date format '{target_date}'. Use ISO format: YYYY-MM-DD",
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if not updates:
|
|
431
|
+
return {
|
|
432
|
+
"status": "error",
|
|
433
|
+
"error": "No updates provided. At least one field (title, description, state, target_date) must be specified.",
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
# Update via adapter
|
|
437
|
+
updated = await adapter.update_epic(epic_id, updates) # type: ignore
|
|
438
|
+
|
|
439
|
+
if updated is None:
|
|
440
|
+
return {
|
|
441
|
+
"status": "error",
|
|
442
|
+
"error": f"Epic {epic_id} not found or update failed",
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
"status": "completed",
|
|
447
|
+
"epic": updated.model_dump(),
|
|
448
|
+
}
|
|
449
|
+
except AttributeError as e:
|
|
450
|
+
return {
|
|
451
|
+
"status": "error",
|
|
452
|
+
"error": f"Epic update method not available: {str(e)}",
|
|
453
|
+
"epic_id": epic_id,
|
|
454
|
+
}
|
|
455
|
+
except Exception as e:
|
|
456
|
+
return {
|
|
457
|
+
"status": "error",
|
|
458
|
+
"error": f"Failed to update epic: {str(e)}",
|
|
459
|
+
"epic_id": epic_id,
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
|
|
314
463
|
@mcp.tool()
|
|
315
464
|
async def hierarchy_tree(
|
|
316
465
|
epic_id: str,
|