agentek-youtrack-mcp 1.0.1 → 1.1.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.
- package/dist/python/main.py +2 -2
- package/dist/python/youtrack_mcp/api/issues.py +16 -0
- package/dist/python/youtrack_mcp/tools/issues/__init__.py +19 -3
- package/dist/python/youtrack_mcp/tools/issues/basic_operations.py +11 -12
- package/dist/python/youtrack_mcp/tools/loader.py +21 -0
- package/dist/python/youtrack_mcp/version.py +1 -1
- package/package.json +1 -1
package/dist/python/main.py
CHANGED
|
@@ -11,7 +11,7 @@ from mcp.server.fastmcp import FastMCP
|
|
|
11
11
|
|
|
12
12
|
from youtrack_mcp.version import __version__ as APP_VERSION
|
|
13
13
|
from youtrack_mcp.config import config
|
|
14
|
-
from youtrack_mcp.tools.loader import load_all_tools
|
|
14
|
+
from youtrack_mcp.tools.loader import load_all_tools, tool_description
|
|
15
15
|
|
|
16
16
|
# Set up logging
|
|
17
17
|
logging.basicConfig(
|
|
@@ -34,7 +34,7 @@ def create_server(host: str = "0.0.0.0", port: int = 8000) -> FastMCP:
|
|
|
34
34
|
# Load and register all tools
|
|
35
35
|
tools = load_all_tools()
|
|
36
36
|
for name, func in tools.items():
|
|
37
|
-
mcp.add_tool(func, name=name)
|
|
37
|
+
mcp.add_tool(func, name=name, description=tool_description(func))
|
|
38
38
|
|
|
39
39
|
logger.info(f"Registered {len(tools)} tools with FastMCP")
|
|
40
40
|
return mcp
|
|
@@ -183,6 +183,7 @@ class IssuesClient:
|
|
|
183
183
|
summary: str,
|
|
184
184
|
description: Optional[str] = None,
|
|
185
185
|
additional_fields: Optional[Dict[str, Any]] = None,
|
|
186
|
+
custom_fields: Optional[Dict[str, Any]] = None,
|
|
186
187
|
) -> Issue:
|
|
187
188
|
"""
|
|
188
189
|
Create a new issue.
|
|
@@ -192,6 +193,9 @@ class IssuesClient:
|
|
|
192
193
|
summary: The issue summary
|
|
193
194
|
description: The issue description
|
|
194
195
|
additional_fields: Additional fields to set on the issue
|
|
196
|
+
custom_fields: Custom field names/values sent in the same POST as
|
|
197
|
+
the issue itself — projects whose required fields have no
|
|
198
|
+
default value reject a create without them
|
|
195
199
|
|
|
196
200
|
Returns:
|
|
197
201
|
The created issue data
|
|
@@ -209,6 +213,11 @@ class IssuesClient:
|
|
|
209
213
|
if description:
|
|
210
214
|
data["description"] = description
|
|
211
215
|
|
|
216
|
+
if custom_fields:
|
|
217
|
+
data.update(
|
|
218
|
+
self._build_custom_fields_payload(custom_fields, project_id)
|
|
219
|
+
)
|
|
220
|
+
|
|
212
221
|
if additional_fields:
|
|
213
222
|
data.update(additional_fields)
|
|
214
223
|
|
|
@@ -266,6 +275,7 @@ class IssuesClient:
|
|
|
266
275
|
issue_id: str,
|
|
267
276
|
summary: Optional[str] = None,
|
|
268
277
|
description: Optional[str] = None,
|
|
278
|
+
uses_markdown: Optional[bool] = None,
|
|
269
279
|
additional_fields: Optional[Dict[str, Any]] = None,
|
|
270
280
|
) -> Issue:
|
|
271
281
|
"""
|
|
@@ -275,6 +285,9 @@ class IssuesClient:
|
|
|
275
285
|
issue_id: The issue ID or readable ID
|
|
276
286
|
summary: The new issue summary
|
|
277
287
|
description: The new issue description
|
|
288
|
+
uses_markdown: Whether the description is rendered as Markdown
|
|
289
|
+
(True) or YouTrack Wiki markup (False). Leave as None to keep
|
|
290
|
+
the issue's current setting.
|
|
278
291
|
additional_fields: Additional fields to update
|
|
279
292
|
|
|
280
293
|
Returns:
|
|
@@ -288,6 +301,9 @@ class IssuesClient:
|
|
|
288
301
|
if description is not None:
|
|
289
302
|
data["description"] = description
|
|
290
303
|
|
|
304
|
+
if uses_markdown is not None:
|
|
305
|
+
data["usesMarkdown"] = uses_markdown
|
|
306
|
+
|
|
291
307
|
if additional_fields:
|
|
292
308
|
data.update(additional_fields)
|
|
293
309
|
|
|
@@ -129,9 +129,25 @@ class IssueTools:
|
|
|
129
129
|
"""Search for issues using YouTrack query syntax."""
|
|
130
130
|
return self.basic_operations.search_issues(query, limit)
|
|
131
131
|
|
|
132
|
-
def create_issue(
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
def create_issue(
|
|
133
|
+
self,
|
|
134
|
+
project: str,
|
|
135
|
+
summary: str,
|
|
136
|
+
description: Optional[str] = None,
|
|
137
|
+
custom_fields: Optional[Dict[str, Any]] = None,
|
|
138
|
+
) -> str:
|
|
139
|
+
"""Create a new issue in the specified project, optionally setting custom fields in the same request.
|
|
140
|
+
|
|
141
|
+
custom_fields maps field names to values and is sent together with the issue itself.
|
|
142
|
+
Projects whose mandatory fields have no default value reject a create without them,
|
|
143
|
+
so pass those fields here instead of updating the issue afterwards.
|
|
144
|
+
Use get_custom_fields / get_available_custom_field_values to discover names and allowed values.
|
|
145
|
+
|
|
146
|
+
Example: create_issue(project="DEMO", summary="Bug in login",
|
|
147
|
+
description="Users cannot log in",
|
|
148
|
+
custom_fields={"Priority": "Critical", "Type": "Bug", "Assignee": "john.doe"})
|
|
149
|
+
"""
|
|
150
|
+
return self.basic_operations.create_issue(project, summary, description, custom_fields)
|
|
135
151
|
|
|
136
152
|
def update_issue(self, issue_id: str, summary: Optional[str] = None, description: Optional[str] = None, uses_markdown: Optional[bool] = None, additional_fields: Optional[Dict[str, Any]] = None) -> str:
|
|
137
153
|
"""Update basic issue fields."""
|
|
@@ -107,7 +107,8 @@ class BasicOperations:
|
|
|
107
107
|
project: The project identifier (e.g., "DEMO", "PROJECT")
|
|
108
108
|
summary: The issue title/summary
|
|
109
109
|
description: Optional detailed description of the issue
|
|
110
|
-
custom_fields: Optional dictionary of custom field names and values
|
|
110
|
+
custom_fields: Optional dictionary of custom field names and values, sent in the same API call as the issue
|
|
111
|
+
(required for projects where mandatory fields have no default value)
|
|
111
112
|
|
|
112
113
|
Returns:
|
|
113
114
|
JSON string with the created issue information
|
|
@@ -161,7 +162,7 @@ class BasicOperations:
|
|
|
161
162
|
# Call the API client to create the issue
|
|
162
163
|
try:
|
|
163
164
|
issue = self.issues_api.create_issue(
|
|
164
|
-
project_id, summary, description
|
|
165
|
+
project_id, summary, description, custom_fields=custom_fields
|
|
165
166
|
)
|
|
166
167
|
|
|
167
168
|
# Check if we got an issue with an ID
|
|
@@ -169,14 +170,6 @@ class BasicOperations:
|
|
|
169
170
|
# Handle error returned as a dict
|
|
170
171
|
return format_json_response(issue)
|
|
171
172
|
|
|
172
|
-
# Apply custom fields if provided
|
|
173
|
-
if custom_fields and hasattr(issue, "id") and issue.id:
|
|
174
|
-
try:
|
|
175
|
-
logger.info(f"Setting custom fields on new issue {issue.id}: {custom_fields}")
|
|
176
|
-
self.issues_api.update_issue_custom_fields(issue.id, custom_fields, validate=False)
|
|
177
|
-
except Exception as cf_err:
|
|
178
|
-
logger.warning(f"Issue created but failed to set custom fields: {cf_err}")
|
|
179
|
-
|
|
180
173
|
# Try to get full issue details right after creation
|
|
181
174
|
if hasattr(issue, "id"):
|
|
182
175
|
try:
|
|
@@ -224,6 +217,7 @@ class BasicOperations:
|
|
|
224
217
|
issue_id: str,
|
|
225
218
|
summary: Optional[str] = None,
|
|
226
219
|
description: Optional[str] = None,
|
|
220
|
+
uses_markdown: Optional[bool] = None,
|
|
227
221
|
additional_fields: Optional[Dict[str, Any]] = None,
|
|
228
222
|
) -> str:
|
|
229
223
|
"""
|
|
@@ -235,6 +229,9 @@ class BasicOperations:
|
|
|
235
229
|
issue_id: The issue identifier (e.g., "DEMO-123", "PROJECT-456")
|
|
236
230
|
summary: The new issue summary/title (optional)
|
|
237
231
|
description: The new issue description (optional)
|
|
232
|
+
uses_markdown: Whether the description is rendered as Markdown
|
|
233
|
+
(True) or YouTrack Wiki markup (False); leave as None to keep
|
|
234
|
+
the current setting (optional)
|
|
238
235
|
additional_fields: Additional fields to update as dict (optional)
|
|
239
236
|
|
|
240
237
|
Returns:
|
|
@@ -245,6 +242,7 @@ class BasicOperations:
|
|
|
245
242
|
issue_id=issue_id,
|
|
246
243
|
summary=summary,
|
|
247
244
|
description=description,
|
|
245
|
+
uses_markdown=uses_markdown,
|
|
248
246
|
additional_fields=additional_fields,
|
|
249
247
|
)
|
|
250
248
|
# Convert Issue object to dict if needed
|
|
@@ -295,12 +293,12 @@ class BasicOperations:
|
|
|
295
293
|
}
|
|
296
294
|
},
|
|
297
295
|
"create_issue": {
|
|
298
|
-
"description": "Create a new issue in YouTrack with automatic project validation. Accepts both project short names (DEMO) and project IDs (0-1).
|
|
296
|
+
"description": "Create a new issue in YouTrack with automatic project validation. Accepts both project short names (DEMO) and project IDs (0-1). Custom fields are sent in the same API call as the issue, so projects with mandatory fields (no default value) can be created in one step. Example: create_issue(project='DEMO', summary='Bug in login', description='Users cannot log in', custom_fields={'Assignee': 'john.doe', 'Priority': 'Critical'})",
|
|
299
297
|
"parameter_descriptions": {
|
|
300
298
|
"project": "Project identifier (short name like 'DEMO' or ID like '0-1')",
|
|
301
299
|
"summary": "Issue title/summary (required)",
|
|
302
300
|
"description": "Detailed description of the issue (optional)",
|
|
303
|
-
"custom_fields": "Optional dictionary of custom field names to values
|
|
301
|
+
"custom_fields": "Optional dictionary of custom field names to values, applied atomically with issue creation (e.g., {'Assignee': 'john.doe', 'Priority': 'Critical', 'Fix versions': ['1.0', '1.1']})"
|
|
304
302
|
}
|
|
305
303
|
},
|
|
306
304
|
"update_issue": {
|
|
@@ -309,6 +307,7 @@ class BasicOperations:
|
|
|
309
307
|
"issue_id": "Issue identifier like 'DEMO-123' or 'PROJECT-456'",
|
|
310
308
|
"summary": "New issue summary/title (optional)",
|
|
311
309
|
"description": "New issue description (optional)",
|
|
310
|
+
"uses_markdown": "Set True to render the description as Markdown, False for YouTrack Wiki markup; omit to keep the current setting (optional)",
|
|
312
311
|
"additional_fields": "Additional fields to update as dictionary (optional)"
|
|
313
312
|
}
|
|
314
313
|
},
|
|
@@ -325,6 +325,27 @@ def load_all_tools() -> Dict[str, Callable]:
|
|
|
325
325
|
return tools
|
|
326
326
|
|
|
327
327
|
|
|
328
|
+
def tool_description(func: Callable) -> Any:
|
|
329
|
+
"""
|
|
330
|
+
Build the description an MCP client sees for a tool.
|
|
331
|
+
|
|
332
|
+
Renders the tool's `tool_definition` (description + parameter_descriptions) into text.
|
|
333
|
+
Returns None when the tool has no definition, so FastMCP falls back to the docstring.
|
|
334
|
+
"""
|
|
335
|
+
definition = getattr(func, "tool_definition", None)
|
|
336
|
+
if not definition or not definition.get("description"):
|
|
337
|
+
return None
|
|
338
|
+
|
|
339
|
+
lines = [definition["description"]]
|
|
340
|
+
parameters = definition.get("parameter_descriptions") or {}
|
|
341
|
+
if parameters:
|
|
342
|
+
lines.append("")
|
|
343
|
+
lines.append("Parameters:")
|
|
344
|
+
lines.extend(f"- {name}: {text}" for name, text in parameters.items())
|
|
345
|
+
|
|
346
|
+
return "\n".join(lines)
|
|
347
|
+
|
|
348
|
+
|
|
328
349
|
def _get_tools_from_class(tool_class: Any) -> Dict[str, Callable]:
|
|
329
350
|
"""
|
|
330
351
|
Get all tools from a tool class.
|