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

Files changed (84) hide show
  1. mcp_ticketer/__version__.py +3 -3
  2. mcp_ticketer/adapters/__init__.py +2 -0
  3. mcp_ticketer/adapters/aitrackdown.py +263 -14
  4. mcp_ticketer/adapters/asana/__init__.py +15 -0
  5. mcp_ticketer/adapters/asana/adapter.py +1308 -0
  6. mcp_ticketer/adapters/asana/client.py +292 -0
  7. mcp_ticketer/adapters/asana/mappers.py +334 -0
  8. mcp_ticketer/adapters/asana/types.py +146 -0
  9. mcp_ticketer/adapters/github.py +326 -109
  10. mcp_ticketer/adapters/hybrid.py +11 -11
  11. mcp_ticketer/adapters/jira.py +271 -25
  12. mcp_ticketer/adapters/linear/adapter.py +693 -39
  13. mcp_ticketer/adapters/linear/client.py +61 -9
  14. mcp_ticketer/adapters/linear/mappers.py +9 -3
  15. mcp_ticketer/adapters/linear/queries.py +9 -7
  16. mcp_ticketer/cache/memory.py +9 -8
  17. mcp_ticketer/cli/adapter_diagnostics.py +1 -1
  18. mcp_ticketer/cli/auggie_configure.py +104 -15
  19. mcp_ticketer/cli/codex_configure.py +188 -32
  20. mcp_ticketer/cli/configure.py +37 -48
  21. mcp_ticketer/cli/diagnostics.py +20 -18
  22. mcp_ticketer/cli/discover.py +292 -26
  23. mcp_ticketer/cli/gemini_configure.py +107 -26
  24. mcp_ticketer/cli/instruction_commands.py +429 -0
  25. mcp_ticketer/cli/linear_commands.py +105 -22
  26. mcp_ticketer/cli/main.py +1830 -435
  27. mcp_ticketer/cli/mcp_configure.py +296 -89
  28. mcp_ticketer/cli/migrate_config.py +12 -8
  29. mcp_ticketer/cli/platform_commands.py +123 -0
  30. mcp_ticketer/cli/platform_detection.py +412 -0
  31. mcp_ticketer/cli/python_detection.py +126 -0
  32. mcp_ticketer/cli/queue_commands.py +15 -15
  33. mcp_ticketer/cli/simple_health.py +1 -1
  34. mcp_ticketer/cli/ticket_commands.py +773 -0
  35. mcp_ticketer/cli/update_checker.py +313 -0
  36. mcp_ticketer/cli/utils.py +67 -62
  37. mcp_ticketer/core/__init__.py +14 -1
  38. mcp_ticketer/core/adapter.py +84 -15
  39. mcp_ticketer/core/config.py +44 -39
  40. mcp_ticketer/core/env_discovery.py +42 -12
  41. mcp_ticketer/core/env_loader.py +15 -14
  42. mcp_ticketer/core/exceptions.py +3 -3
  43. mcp_ticketer/core/http_client.py +26 -26
  44. mcp_ticketer/core/instructions.py +405 -0
  45. mcp_ticketer/core/mappers.py +11 -11
  46. mcp_ticketer/core/models.py +50 -20
  47. mcp_ticketer/core/onepassword_secrets.py +379 -0
  48. mcp_ticketer/core/project_config.py +57 -35
  49. mcp_ticketer/core/registry.py +3 -3
  50. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  51. mcp_ticketer/mcp/__init__.py +29 -1
  52. mcp_ticketer/mcp/__main__.py +60 -0
  53. mcp_ticketer/mcp/server/__init__.py +25 -0
  54. mcp_ticketer/mcp/server/__main__.py +60 -0
  55. mcp_ticketer/mcp/{dto.py → server/dto.py} +32 -32
  56. mcp_ticketer/mcp/{server.py → server/main.py} +127 -74
  57. mcp_ticketer/mcp/{response_builder.py → server/response_builder.py} +2 -2
  58. mcp_ticketer/mcp/server/server_sdk.py +93 -0
  59. mcp_ticketer/mcp/server/tools/__init__.py +47 -0
  60. mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
  61. mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
  62. mcp_ticketer/mcp/server/tools/comment_tools.py +90 -0
  63. mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
  64. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +532 -0
  65. mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
  66. mcp_ticketer/mcp/server/tools/pr_tools.py +154 -0
  67. mcp_ticketer/mcp/server/tools/search_tools.py +206 -0
  68. mcp_ticketer/mcp/server/tools/ticket_tools.py +430 -0
  69. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
  70. mcp_ticketer/queue/__init__.py +1 -0
  71. mcp_ticketer/queue/health_monitor.py +5 -4
  72. mcp_ticketer/queue/manager.py +15 -51
  73. mcp_ticketer/queue/queue.py +19 -19
  74. mcp_ticketer/queue/run_worker.py +1 -1
  75. mcp_ticketer/queue/ticket_registry.py +14 -14
  76. mcp_ticketer/queue/worker.py +16 -14
  77. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +168 -32
  78. mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
  79. mcp_ticketer-0.3.5.dist-info/RECORD +0 -62
  80. /mcp_ticketer/mcp/{constants.py → server/constants.py} +0 -0
  81. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
  82. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
  83. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
  84. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,90 @@
1
+ """Comment management tools for tickets.
2
+
3
+ This module implements tools for adding and retrieving comments on tickets.
4
+ """
5
+
6
+ from typing import Any
7
+
8
+ from ....core.models import Comment
9
+ from ..server_sdk import get_adapter, mcp
10
+
11
+
12
+ @mcp.tool()
13
+ async def ticket_comment(
14
+ ticket_id: str,
15
+ operation: str,
16
+ text: str | None = None,
17
+ limit: int = 10,
18
+ offset: int = 0,
19
+ ) -> dict[str, Any]:
20
+ """Add or list comments on a ticket.
21
+
22
+ This tool supports two operations:
23
+ - 'add': Add a new comment to a ticket (requires 'text' parameter)
24
+ - 'list': Retrieve comments from a ticket (supports pagination)
25
+
26
+ Args:
27
+ ticket_id: Unique identifier of the ticket
28
+ operation: Operation to perform - must be 'add' or 'list'
29
+ text: Comment text (required when operation='add')
30
+ limit: Maximum number of comments to return (used when operation='list', default: 10)
31
+ offset: Number of comments to skip for pagination (used when operation='list', default: 0)
32
+
33
+ Returns:
34
+ Comment data or list of comments, or error information
35
+
36
+ """
37
+ try:
38
+ adapter = get_adapter()
39
+
40
+ # Validate operation
41
+ if operation not in ["add", "list"]:
42
+ return {
43
+ "status": "error",
44
+ "error": f"Invalid operation '{operation}'. Must be 'add' or 'list'",
45
+ }
46
+
47
+ if operation == "add":
48
+ # Add comment operation
49
+ if not text:
50
+ return {
51
+ "status": "error",
52
+ "error": "Parameter 'text' is required when operation='add'",
53
+ }
54
+
55
+ # Create comment object
56
+ comment = Comment(
57
+ ticket_id=ticket_id,
58
+ content=text,
59
+ )
60
+
61
+ # Add comment via adapter
62
+ created = await adapter.add_comment(comment)
63
+
64
+ return {
65
+ "status": "completed",
66
+ "operation": "add",
67
+ "comment": created.model_dump(),
68
+ }
69
+
70
+ else: # operation == "list"
71
+ # List comments operation
72
+ comments = await adapter.get_comments(
73
+ ticket_id=ticket_id, limit=limit, offset=offset
74
+ )
75
+
76
+ return {
77
+ "status": "completed",
78
+ "operation": "list",
79
+ "ticket_id": ticket_id,
80
+ "comments": [comment.model_dump() for comment in comments],
81
+ "count": len(comments),
82
+ "limit": limit,
83
+ "offset": offset,
84
+ }
85
+
86
+ except Exception as e:
87
+ return {
88
+ "status": "error",
89
+ "error": f"Comment operation failed: {str(e)}",
90
+ }
@@ -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