agentek-youtrack-mcp 1.0.2 → 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 +9 -0
- package/dist/python/youtrack_mcp/tools/issues/__init__.py +19 -3
- package/dist/python/youtrack_mcp/tools/issues/basic_operations.py +5 -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
|
|
|
@@ -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:
|
|
@@ -300,12 +293,12 @@ class BasicOperations:
|
|
|
300
293
|
}
|
|
301
294
|
},
|
|
302
295
|
"create_issue": {
|
|
303
|
-
"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'})",
|
|
304
297
|
"parameter_descriptions": {
|
|
305
298
|
"project": "Project identifier (short name like 'DEMO' or ID like '0-1')",
|
|
306
299
|
"summary": "Issue title/summary (required)",
|
|
307
300
|
"description": "Detailed description of the issue (optional)",
|
|
308
|
-
"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']})"
|
|
309
302
|
}
|
|
310
303
|
},
|
|
311
304
|
"update_issue": {
|
|
@@ -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.
|