agentek-youtrack-mcp 1.0.0

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.
Files changed (44) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +332 -0
  3. package/README.npm.md +436 -0
  4. package/dist/bin/youtrack-mcp.js +158 -0
  5. package/dist/index.js +129 -0
  6. package/dist/python/main.py +74 -0
  7. package/dist/python/requirements.txt +13 -0
  8. package/dist/python/youtrack_mcp/__init__.py +5 -0
  9. package/dist/python/youtrack_mcp/__pycache__/__init__.cpython-311.pyc +0 -0
  10. package/dist/python/youtrack_mcp/__pycache__/version.cpython-311.pyc +0 -0
  11. package/dist/python/youtrack_mcp/api/__init__.py +3 -0
  12. package/dist/python/youtrack_mcp/api/articles.py +317 -0
  13. package/dist/python/youtrack_mcp/api/client.py +466 -0
  14. package/dist/python/youtrack_mcp/api/issues.py +3008 -0
  15. package/dist/python/youtrack_mcp/api/mcp_wrappers.py +304 -0
  16. package/dist/python/youtrack_mcp/api/projects.py +1009 -0
  17. package/dist/python/youtrack_mcp/api/search.py +244 -0
  18. package/dist/python/youtrack_mcp/api/spaces.py +46 -0
  19. package/dist/python/youtrack_mcp/api/users.py +149 -0
  20. package/dist/python/youtrack_mcp/config.py +273 -0
  21. package/dist/python/youtrack_mcp/mcp_server.py +158 -0
  22. package/dist/python/youtrack_mcp/mcp_wrappers.py +324 -0
  23. package/dist/python/youtrack_mcp/tools/__init__.py +8 -0
  24. package/dist/python/youtrack_mcp/tools/articles.py +487 -0
  25. package/dist/python/youtrack_mcp/tools/create_project_tool.py +51 -0
  26. package/dist/python/youtrack_mcp/tools/issues/__init__.py +208 -0
  27. package/dist/python/youtrack_mcp/tools/issues/attachments.py +161 -0
  28. package/dist/python/youtrack_mcp/tools/issues/basic_operations.py +322 -0
  29. package/dist/python/youtrack_mcp/tools/issues/custom_fields.py +365 -0
  30. package/dist/python/youtrack_mcp/tools/issues/dedicated_updates.py +601 -0
  31. package/dist/python/youtrack_mcp/tools/issues/diagnostics.py +363 -0
  32. package/dist/python/youtrack_mcp/tools/issues/linking.py +283 -0
  33. package/dist/python/youtrack_mcp/tools/issues/utilities.py +291 -0
  34. package/dist/python/youtrack_mcp/tools/loader.py +383 -0
  35. package/dist/python/youtrack_mcp/tools/projects.py +826 -0
  36. package/dist/python/youtrack_mcp/tools/projects.py-e +411 -0
  37. package/dist/python/youtrack_mcp/tools/resources.py +744 -0
  38. package/dist/python/youtrack_mcp/tools/search.py +297 -0
  39. package/dist/python/youtrack_mcp/tools/spaces.py +69 -0
  40. package/dist/python/youtrack_mcp/tools/users.py +185 -0
  41. package/dist/python/youtrack_mcp/utils.py +87 -0
  42. package/dist/python/youtrack_mcp/version.py +5 -0
  43. package/package.json +61 -0
  44. package/requirements.txt +13 -0
@@ -0,0 +1,244 @@
1
+ """
2
+ Search functionality for YouTrack API.
3
+ """
4
+
5
+ import logging
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ from youtrack_mcp.api.client import YouTrackClient
9
+
10
+
11
+ class SearchClient:
12
+ """Client for advanced search operations in YouTrack."""
13
+
14
+ def __init__(self, client: YouTrackClient):
15
+ """
16
+ Initialize the Search API client.
17
+
18
+ Args:
19
+ client: The YouTrack API client
20
+ """
21
+ self.client = client
22
+
23
+ def search_issues(
24
+ self,
25
+ query: str,
26
+ fields: Optional[List[str]] = None,
27
+ limit: int = 10,
28
+ offset: int = 0,
29
+ sort_by: Optional[str] = None,
30
+ sort_order: Optional[str] = None,
31
+ custom_fields: Optional[List[str]] = None,
32
+ ) -> List[Dict[str, Any]]:
33
+ """
34
+ Search for issues with advanced query capabilities.
35
+
36
+ Args:
37
+ query: The YouTrack query string
38
+ fields: List of fields to include in the response (default: all fields)
39
+ limit: Maximum number of issues to return
40
+ offset: Offset for pagination
41
+ sort_by: Field to sort by (e.g., 'created', 'updated', 'priority')
42
+ sort_order: Sort order ('asc' or 'desc')
43
+ custom_fields: List of custom field names to include
44
+
45
+ Returns:
46
+ List of matching issues with requested fields
47
+ """
48
+ # Base set of fields to retrieve
49
+ default_fields = [
50
+ "id",
51
+ "idReadable",
52
+ "summary",
53
+ "description",
54
+ "created",
55
+ "updated",
56
+ "resolved",
57
+ "priority",
58
+ "project(id,name,shortName)",
59
+ "reporter(id,login,name)",
60
+ "assignee(id,login,name)",
61
+ ]
62
+
63
+ # Combine default fields with requested fields
64
+ if fields:
65
+ requested_fields = default_fields + fields
66
+ else:
67
+ requested_fields = default_fields
68
+
69
+ # Add custom fields if requested
70
+ if custom_fields:
71
+ # Build the customFields part with specific field names
72
+ cf_string = ",".join(
73
+ [f"customFields(id,name,value)" for _ in custom_fields]
74
+ )
75
+ requested_fields.append(cf_string)
76
+ else:
77
+ # By default, include all custom fields
78
+ requested_fields.append("customFields(id,name,value)")
79
+
80
+ # Build the fields parameter
81
+ fields_param = ",".join(requested_fields)
82
+
83
+ # Build params dictionary
84
+ params = {
85
+ "query": query,
86
+ "$top": limit,
87
+ "$skip": offset,
88
+ "fields": fields_param,
89
+ }
90
+
91
+ # Add sort parameters if provided
92
+ if sort_by:
93
+ params["$sort"] = sort_by
94
+ if sort_order and sort_order.lower() in ("asc", "desc"):
95
+ params["$sortOrder"] = sort_order.lower()
96
+
97
+ # Make the API request
98
+ return self.client.get("issues", params=params)
99
+
100
+ def search_with_custom_field_values(
101
+ self, query: str, custom_field_values: Dict[str, Any], limit: int = 10
102
+ ) -> List[Dict[str, Any]]:
103
+ """
104
+ Search for issues with specific custom field values.
105
+
106
+ Args:
107
+ query: The base YouTrack query string
108
+ custom_field_values: Dictionary mapping custom field names to values
109
+ limit: Maximum number of issues to return
110
+
111
+ Returns:
112
+ List of matching issues
113
+ """
114
+ # Build query with custom field conditions
115
+ enhanced_query = query
116
+
117
+ for field_name, field_value in custom_field_values.items():
118
+ if field_value is not None:
119
+ # Handle different types of values
120
+ if isinstance(field_value, bool):
121
+ value_str = "true" if field_value else "false"
122
+ elif isinstance(field_value, (int, float)):
123
+ value_str = str(field_value)
124
+ elif isinstance(field_value, list):
125
+ # For multi-value fields, we use 'in' operator
126
+ values = ", ".join([f'"{val}"' for val in field_value])
127
+ enhanced_query += f" {field_name} in ({values})"
128
+ continue
129
+ else:
130
+ # String values need to be quoted
131
+ value_str = f'"{field_value}"'
132
+
133
+ # Add the condition to the query
134
+ enhanced_query += f" {field_name}: {value_str}"
135
+
136
+ # Call the search_issues method with the enhanced query
137
+ return self.search_issues(enhanced_query, limit=limit)
138
+
139
+ def search_with_filter(
140
+ self,
141
+ project: Optional[str] = None,
142
+ author: Optional[str] = None,
143
+ assignee: Optional[str] = None,
144
+ state: Optional[str] = None,
145
+ priority: Optional[str] = None,
146
+ text: Optional[str] = None,
147
+ created_after: Optional[str] = None,
148
+ created_before: Optional[str] = None,
149
+ updated_after: Optional[str] = None,
150
+ updated_before: Optional[str] = None,
151
+ custom_fields: Optional[Dict[str, Any]] = None,
152
+ limit: int = 10,
153
+ ) -> List[Dict[str, Any]]:
154
+ """
155
+ Search for issues using a structured filter approach.
156
+
157
+ Args:
158
+ project: Project ID or name
159
+ author: Issue author (reporter) login or name
160
+ assignee: Issue assignee login or name
161
+ state: Issue state (e.g., "Open", "Resolved")
162
+ priority: Issue priority
163
+ text: Text to search in summary and description
164
+ created_after: Issues created after this date (YYYY-MM-DD)
165
+ created_before: Issues created before this date (YYYY-MM-DD)
166
+ updated_after: Issues updated after this date (YYYY-MM-DD)
167
+ updated_before: Issues updated before this date (YYYY-MM-DD)
168
+ custom_fields: Dictionary mapping custom field names to values
169
+ limit: Maximum number of issues to return
170
+
171
+ Returns:
172
+ List of matching issues
173
+ """
174
+ # Build query based on provided filters
175
+ query_parts = []
176
+
177
+ if project:
178
+ query_parts.append(f'project: "{project}"')
179
+
180
+ if author:
181
+ query_parts.append(f'reporter: "{author}"')
182
+
183
+ if assignee:
184
+ if assignee.lower() == "unassigned":
185
+ query_parts.append("assignee: Unassigned")
186
+ else:
187
+ query_parts.append(f'assignee: "{assignee}"')
188
+
189
+ if state:
190
+ query_parts.append(f'State: "{state}"')
191
+
192
+ if priority:
193
+ query_parts.append(f'Priority: "{priority}"')
194
+
195
+ if text:
196
+ # Search in summary and description
197
+ query_parts.append(f'summary: "{text}" description: "{text}"')
198
+
199
+ if created_after:
200
+ query_parts.append(f"created: {created_after} ..")
201
+
202
+ if created_before:
203
+ query_parts.append(f"created: .. {created_before}")
204
+
205
+ if updated_after:
206
+ query_parts.append(f"updated: {updated_after} ..")
207
+
208
+ if updated_before:
209
+ query_parts.append(f"updated: .. {updated_before}")
210
+
211
+ # Combine all parts into a single query
212
+ base_query = " ".join(query_parts)
213
+
214
+ # If custom fields are provided, use the specialized method
215
+ if custom_fields:
216
+ return self.search_with_custom_field_values(
217
+ base_query, custom_fields, limit
218
+ )
219
+ else:
220
+ return self.search_issues(base_query, limit=limit)
221
+
222
+ def get_available_custom_fields(
223
+ self, project_id: Optional[str] = None
224
+ ) -> List[Dict[str, Any]]:
225
+ """
226
+ Get all available custom fields, optionally for a specific project.
227
+
228
+ Args:
229
+ project_id: Optional project ID to get fields for a specific project
230
+
231
+ Returns:
232
+ List of custom fields with their properties
233
+ """
234
+ if project_id:
235
+ # Get custom fields for a specific project
236
+ endpoint = f"admin/projects/{project_id}/customFields"
237
+ else:
238
+ # Get all custom fields in the system
239
+ endpoint = "admin/customFieldSettings/customFields"
240
+
241
+ fields = "id,name,localizedName,fieldType(id,name),isPrivate,isPublic,aliases"
242
+ params = {"fields": fields}
243
+
244
+ return self.client.get(endpoint, params=params)
@@ -0,0 +1,46 @@
1
+ """
2
+ YouTrack Knowledge Base Spaces API client.
3
+ """
4
+
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from pydantic import BaseModel
8
+
9
+ from youtrack_mcp.api.client import YouTrackClient
10
+
11
+
12
+ class Space(BaseModel):
13
+ """Model for a YouTrack Knowledge Base Space."""
14
+
15
+ id: str
16
+ name: Optional[str] = None
17
+
18
+ model_config = {
19
+ "extra": "allow",
20
+ "populate_by_name": True,
21
+ }
22
+
23
+
24
+ class SpacesClient:
25
+ """Client for interacting with YouTrack KB Spaces API."""
26
+
27
+ def __init__(self, client: YouTrackClient):
28
+ self.client = client
29
+
30
+ def get_space(self, space_id: str, fields: Optional[str] = None) -> Space:
31
+ """Get a single space by id."""
32
+ fields_query = fields or "id,name"
33
+ response = self.client.get(f"spaces/{space_id}?fields={fields_query}")
34
+ return Space.model_validate(response)
35
+
36
+ def list_spaces(
37
+ self, fields: Optional[str] = None, top: int = 50, skip: int = 0
38
+ ) -> List[Space]:
39
+ """List spaces with pagination."""
40
+ params: Dict[str, Any] = {
41
+ "$top": top,
42
+ "$skip": skip,
43
+ "fields": fields or "id,name",
44
+ }
45
+ response = self.client.get("spaces", params=params)
46
+ return [Space.model_validate(item) for item in response]
@@ -0,0 +1,149 @@
1
+ """
2
+ YouTrack Users API client.
3
+ """
4
+
5
+ import logging
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ from pydantic import BaseModel
9
+
10
+ from youtrack_mcp.api.client import YouTrackClient
11
+
12
+
13
+ class User(BaseModel):
14
+ """Model for a YouTrack user."""
15
+
16
+ id: str
17
+ login: Optional[str] = None
18
+ name: Optional[str] = None
19
+ email: Optional[str] = None
20
+ jabber: Optional[str] = None
21
+ ring_id: Optional[str] = None
22
+ guest: Optional[bool] = None
23
+ online: Optional[bool] = None
24
+ banned: Optional[bool] = None
25
+
26
+ model_config = {
27
+ "extra": "allow", # Allow extra fields from the API
28
+ "populate_by_name": True, # Allow population by field name (helps with aliases)
29
+ }
30
+
31
+
32
+ class UsersClient:
33
+ """Client for interacting with YouTrack Users API."""
34
+
35
+ def __init__(self, client: YouTrackClient):
36
+ """
37
+ Initialize the Users API client.
38
+
39
+ Args:
40
+ client: The YouTrack API client
41
+ """
42
+ self.client = client
43
+
44
+ def get_current_user(self) -> User:
45
+ """
46
+ Get the current authenticated user.
47
+
48
+ Returns:
49
+ The user data
50
+ """
51
+ fields = "id,login,name,email,jabber,ringId,guest,online,banned"
52
+ response = self.client.get(f"users/me?fields={fields}")
53
+ return User.model_validate(response)
54
+
55
+ def get_user(self, user_id: str) -> User:
56
+ """
57
+ Get a user by ID.
58
+
59
+ Args:
60
+ user_id: The user ID
61
+
62
+ Returns:
63
+ The user data
64
+ """
65
+ fields = "id,login,name,email,jabber,ringId,guest,online,banned"
66
+ response = self.client.get(f"users/{user_id}?fields={fields}")
67
+ return User.model_validate(response)
68
+
69
+ def search_users(self, query: str, limit: int = 10) -> List[User]:
70
+ """
71
+ Search for users.
72
+
73
+ Args:
74
+ query: The search query (name, login, or email)
75
+ limit: Maximum number of users to return
76
+
77
+ Returns:
78
+ List of matching users
79
+ """
80
+ # Request additional fields to ensure we get complete user data
81
+ fields = "id,login,name,email,jabber,ringId,guest,online,banned"
82
+ params = {"query": query, "$top": limit, "fields": fields}
83
+ response = self.client.get("users", params=params)
84
+
85
+ users = []
86
+ for item in response:
87
+ try:
88
+ users.append(User.model_validate(item))
89
+ except Exception as e:
90
+ # Log the error but continue processing other users
91
+ logging.getLogger(__name__).warning(
92
+ f"Failed to validate user: {str(e)}"
93
+ )
94
+
95
+ return users
96
+
97
+ def get_user_by_login(self, login: str) -> Optional[User]:
98
+ """
99
+ Get a user by login name.
100
+
101
+ Args:
102
+ login: The user login name
103
+
104
+ Returns:
105
+ The user data or None if not found
106
+ """
107
+ # Search for the exact login
108
+ users = self.search_users(f"login: {login}", limit=1)
109
+ return users[0] if users else None
110
+
111
+ def get_user_groups(self, user_id: str) -> List[Dict[str, Any]]:
112
+ """
113
+ Get groups for a user.
114
+
115
+ Args:
116
+ user_id: The user ID
117
+
118
+ Returns:
119
+ List of group data
120
+ """
121
+ response = self.client.get(f"users/{user_id}/groups")
122
+ return response
123
+
124
+ def check_user_permissions(self, user_id: str, permission: str) -> bool:
125
+ """
126
+ Check if a user has a specific permission.
127
+
128
+ Args:
129
+ user_id: The user ID
130
+ permission: The permission to check
131
+
132
+ Returns:
133
+ True if the user has the permission, False otherwise
134
+ """
135
+ try:
136
+ # YouTrack doesn't have a direct API for checking permissions,
137
+ # but we can check user's groups and infer permissions
138
+ groups = self.get_user_groups(user_id)
139
+
140
+ # Different permissions might require different group checks
141
+ # This is a simplified example
142
+ for group in groups:
143
+ if permission.lower() in (group.get("name", "").lower() or ""):
144
+ return True
145
+
146
+ return False
147
+ except Exception:
148
+ # If we can't determine, assume no permission
149
+ return False