agentek-youtrack-mcp 1.0.0 → 1.0.2
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/README.md +6 -45
- package/README.npm.md +5 -71
- package/dist/bin/youtrack-mcp.js +1 -1
- package/dist/index.js +10 -2
- package/dist/python/youtrack_mcp/api/issues.py +7 -0
- package/dist/python/youtrack_mcp/tools/issues/basic_operations.py +6 -0
- package/dist/python/youtrack_mcp/version.py +1 -1
- package/package.json +1 -1
- package/dist/python/youtrack_mcp/__pycache__/__init__.cpython-311.pyc +0 -0
- package/dist/python/youtrack_mcp/__pycache__/version.cpython-311.pyc +0 -0
- package/dist/python/youtrack_mcp/tools/projects.py-e +0 -411
package/README.md
CHANGED
|
@@ -180,42 +180,29 @@ Choose from multiple registries:
|
|
|
180
180
|
|
|
181
181
|
#### Docker Hub (Primary)
|
|
182
182
|
```bash
|
|
183
|
-
# Use the latest
|
|
183
|
+
# Use the latest release
|
|
184
184
|
docker run --rm \
|
|
185
185
|
-e YOUTRACK_URL="https://your-instance.youtrack.cloud" \
|
|
186
186
|
-e YOUTRACK_API_TOKEN="your-token" \
|
|
187
187
|
windbit/agentek-youtrack-mcp:latest
|
|
188
|
-
|
|
189
|
-
# Or use the latest development build
|
|
190
|
-
docker run --rm \
|
|
191
|
-
-e YOUTRACK_URL="https://your-instance.youtrack.cloud" \
|
|
192
|
-
-e YOUTRACK_API_TOKEN="your-token" \
|
|
193
|
-
windbit/agentek-youtrack-mcp:1.1.2_wip
|
|
194
188
|
```
|
|
195
189
|
|
|
196
|
-
#### GitHub Container Registry
|
|
190
|
+
#### GitHub Container Registry
|
|
197
191
|
```bash
|
|
198
|
-
# Use the latest
|
|
192
|
+
# Use the latest release
|
|
199
193
|
docker run --rm \
|
|
200
194
|
-e YOUTRACK_URL="https://your-instance.youtrack.cloud" \
|
|
201
195
|
-e YOUTRACK_API_TOKEN="your-token" \
|
|
202
196
|
ghcr.io/windbit/agentek-youtrack-mcp:latest
|
|
203
|
-
|
|
204
|
-
# Or use the latest development build
|
|
205
|
-
docker run --rm \
|
|
206
|
-
-e YOUTRACK_URL="https://your-instance.youtrack.cloud" \
|
|
207
|
-
-e YOUTRACK_API_TOKEN="your-token" \
|
|
208
|
-
ghcr.io/windbit/agentek-youtrack-mcp:1.1.2_wip
|
|
209
197
|
```
|
|
210
198
|
|
|
211
199
|
### Available Docker Tags
|
|
212
200
|
|
|
213
201
|
Both registries provide identical tags:
|
|
214
202
|
|
|
215
|
-
- `latest` - Latest
|
|
216
|
-
- `1.
|
|
217
|
-
-
|
|
218
|
-
- `pr-<number>` - Pull request builds for testing
|
|
203
|
+
- `latest` - Latest build from the main branch
|
|
204
|
+
- `1.0.0` - Specific release version tags
|
|
205
|
+
- `<commit-sha>` - Exact commit builds
|
|
219
206
|
|
|
220
207
|
*Note: Images are now published to both Docker Hub and GitHub Container Registry simultaneously.*
|
|
221
208
|
|
|
@@ -232,18 +219,6 @@ npm install -g agentek-youtrack-mcp
|
|
|
232
219
|
npx agentek-youtrack-mcp
|
|
233
220
|
```
|
|
234
221
|
|
|
235
|
-
#### GitHub Packages (New)
|
|
236
|
-
```bash
|
|
237
|
-
# Configure GitHub registry
|
|
238
|
-
npm config set @windbit:registry https://npm.pkg.github.com
|
|
239
|
-
|
|
240
|
-
# Install globally
|
|
241
|
-
npm install -g @windbit/agentek-youtrack-mcp
|
|
242
|
-
|
|
243
|
-
# Or use with npx
|
|
244
|
-
npx @windbit/agentek-youtrack-mcp
|
|
245
|
-
```
|
|
246
|
-
|
|
247
222
|
## Features
|
|
248
223
|
|
|
249
224
|
- **Issue Management**: Create, read, update, and delete YouTrack issues
|
|
@@ -316,17 +291,3 @@ For issues and questions:
|
|
|
316
291
|
1. Check the [Issues](https://github.com/windbit/agentek-youtrack-mcp/issues) page
|
|
317
292
|
2. Review the documentation
|
|
318
293
|
3. Submit a new issue with detailed information
|
|
319
|
-
|
|
320
|
-
---
|
|
321
|
-
|
|
322
|
-
*Latest update: Comprehensive custom fields management with 567 test coverage and clean project organization.*
|
|
323
|
-
|
|
324
|
-
## Version 1.11.1 Released
|
|
325
|
-
|
|
326
|
-
🎉 **MAJOR FEATURE** - Custom Fields Management Support
|
|
327
|
-
- ✅ Complete custom fields CRUD operations (create, read, update, delete)
|
|
328
|
-
- ✅ Field validation against project schema (all field types supported)
|
|
329
|
-
- ✅ Batch update capabilities for performance
|
|
330
|
-
- ✅ Comprehensive error handling with detailed messages
|
|
331
|
-
- ✅ 567 tests (+68 new tests) with extensive coverage
|
|
332
|
-
- ✅ Clean project organization with `automations/` directory
|
package/README.npm.md
CHANGED
|
@@ -36,24 +36,17 @@ npx agentek-youtrack-mcp --help
|
|
|
36
36
|
|
|
37
37
|
**Using Docker Hub:**
|
|
38
38
|
```bash
|
|
39
|
-
# Use the latest
|
|
39
|
+
# Use the latest release
|
|
40
40
|
docker run --rm \
|
|
41
41
|
-e YOUTRACK_URL="https://your.youtrack.cloud" \
|
|
42
42
|
-e YOUTRACK_API_TOKEN="your-token" \
|
|
43
43
|
windbit/agentek-youtrack-mcp:latest
|
|
44
|
-
|
|
45
|
-
# Or use the latest development build
|
|
46
|
-
docker run --rm \
|
|
47
|
-
-e YOUTRACK_URL="https://your.youtrack.cloud" \
|
|
48
|
-
-e YOUTRACK_API_TOKEN="your-token" \
|
|
49
|
-
windbit/agentek-youtrack-mcp:1.11.1_wip
|
|
50
44
|
```
|
|
51
45
|
|
|
52
46
|
**Available Docker Tags:**
|
|
53
|
-
- `latest` - Latest
|
|
54
|
-
- `1.
|
|
55
|
-
-
|
|
56
|
-
- `pr-<number>` - Pull request builds for testing
|
|
47
|
+
- `latest` - Latest build from the main branch
|
|
48
|
+
- `1.0.0` - Specific release version tags
|
|
49
|
+
- `<commit-sha>` - Exact commit builds
|
|
57
50
|
|
|
58
51
|
## Configuration
|
|
59
52
|
|
|
@@ -121,22 +114,6 @@ Choose your preferred method:
|
|
|
121
114
|
}
|
|
122
115
|
```
|
|
123
116
|
|
|
124
|
-
#### Using GitHub Packages npm:
|
|
125
|
-
```json
|
|
126
|
-
{
|
|
127
|
-
"mcpServers": {
|
|
128
|
-
"youtrack": {
|
|
129
|
-
"command": "npx",
|
|
130
|
-
"args": ["@windbit/agentek-youtrack-mcp"],
|
|
131
|
-
"env": {
|
|
132
|
-
"YOUTRACK_URL": "https://your.youtrack.cloud",
|
|
133
|
-
"YOUTRACK_API_TOKEN": "your-token"
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
```
|
|
139
|
-
|
|
140
117
|
## Features
|
|
141
118
|
|
|
142
119
|
- **Issue Management**: Create, read, update, and delete YouTrack issues
|
|
@@ -222,15 +199,6 @@ docker run --rm \
|
|
|
222
199
|
ghcr.io/windbit/agentek-youtrack-mcp:latest
|
|
223
200
|
```
|
|
224
201
|
|
|
225
|
-
#### GitHub Packages npm:
|
|
226
|
-
```bash
|
|
227
|
-
# Set up GitHub registry (one-time setup)
|
|
228
|
-
npm config set @windbit:registry https://npm.pkg.github.com
|
|
229
|
-
|
|
230
|
-
# Run with npx
|
|
231
|
-
npx @windbit/agentek-youtrack-mcp
|
|
232
|
-
```
|
|
233
|
-
|
|
234
202
|
### HTTP Mode
|
|
235
203
|
|
|
236
204
|
#### npm/Node.js:
|
|
@@ -266,15 +234,6 @@ docker run --rm -p 8000:8000 \
|
|
|
266
234
|
curl http://localhost:8000/api/tools
|
|
267
235
|
```
|
|
268
236
|
|
|
269
|
-
#### GitHub Packages npm:
|
|
270
|
-
```bash
|
|
271
|
-
# Start HTTP server
|
|
272
|
-
npx @windbit/agentek-youtrack-mcp --http --port 8000
|
|
273
|
-
|
|
274
|
-
# Test the server
|
|
275
|
-
curl http://localhost:8000/api/tools
|
|
276
|
-
```
|
|
277
|
-
|
|
278
237
|
### Development Mode
|
|
279
238
|
|
|
280
239
|
#### npm/Node.js:
|
|
@@ -301,12 +260,6 @@ docker run --rm \
|
|
|
301
260
|
ghcr.io/windbit/agentek-youtrack-mcp:latest --log-level DEBUG
|
|
302
261
|
```
|
|
303
262
|
|
|
304
|
-
#### GitHub Packages npm:
|
|
305
|
-
```bash
|
|
306
|
-
# Run with debug logging
|
|
307
|
-
npx @windbit/agentek-youtrack-mcp -- --log-level DEBUG
|
|
308
|
-
```
|
|
309
|
-
|
|
310
263
|
## Troubleshooting
|
|
311
264
|
|
|
312
265
|
### npm/Node.js Issues
|
|
@@ -367,23 +320,6 @@ docker login ghcr.io
|
|
|
367
320
|
docker pull ghcr.io/windbit/agentek-youtrack-mcp:latest
|
|
368
321
|
```
|
|
369
322
|
|
|
370
|
-
### GitHub Packages Issues
|
|
371
|
-
|
|
372
|
-
#### npm Registry Configuration
|
|
373
|
-
```bash
|
|
374
|
-
# If @windbit/agentek-youtrack-mcp is not found, configure the registry
|
|
375
|
-
npm config set @windbit:registry https://npm.pkg.github.com
|
|
376
|
-
|
|
377
|
-
# You might need authentication for GitHub Packages npm
|
|
378
|
-
npm login --registry=https://npm.pkg.github.com
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
#### Package Not Found
|
|
382
|
-
If GitHub Packages npm shows "package not found":
|
|
383
|
-
- The package is published only on releases
|
|
384
|
-
- Check if a release has been created
|
|
385
|
-
- Fallback to npmjs.org version: `npm install -g agentek-youtrack-mcp`
|
|
386
|
-
|
|
387
323
|
### Common Issues (Both Options)
|
|
388
324
|
|
|
389
325
|
#### Connection Issues
|
|
@@ -407,8 +343,7 @@ If you can't connect to YouTrack:
|
|
|
407
343
|
- ✅ Need to modify or debug the server
|
|
408
344
|
|
|
409
345
|
**Registry Options:**
|
|
410
|
-
- **npmjs.org
|
|
411
|
-
- **GitHub Packages**: `@windbit/agentek-youtrack-mcp` - Integrated with GitHub, requires registry config
|
|
346
|
+
- **npmjs.org**: `agentek-youtrack-mcp`
|
|
412
347
|
|
|
413
348
|
### Choose **Docker** if you:
|
|
414
349
|
- ✅ Want isolated, consistent environments
|
|
@@ -429,7 +364,6 @@ If you can't connect to YouTrack:
|
|
|
429
364
|
- **Docker Hub**: [windbit/agentek-youtrack-mcp](https://hub.docker.com/r/windbit/agentek-youtrack-mcp)
|
|
430
365
|
- **GitHub Container Registry**: [ghcr.io/windbit/agentek-youtrack-mcp](https://github.com/windbit/agentek-youtrack-mcp/pkgs/container/youtrack-mcp)
|
|
431
366
|
- **npmjs.org**: [agentek-youtrack-mcp](https://www.npmjs.com/package/agentek-youtrack-mcp)
|
|
432
|
-
- **GitHub Packages npm**: [@windbit/agentek-youtrack-mcp](https://github.com/windbit/agentek-youtrack-mcp/pkgs/npm/%40windbit%2Fagentek-youtrack-mcp)
|
|
433
367
|
|
|
434
368
|
## License
|
|
435
369
|
|
package/dist/bin/youtrack-mcp.js
CHANGED
|
@@ -73,7 +73,7 @@ function showVersion() {
|
|
|
73
73
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
74
74
|
console.log(`YouTrack MCP Server v${packageJson.version}`);
|
|
75
75
|
} catch (error) {
|
|
76
|
-
console.log('YouTrack MCP Server
|
|
76
|
+
console.log('YouTrack MCP Server (version unknown)');
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { spawn } from 'child_process';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { dirname, join } from 'path';
|
|
6
|
-
import { existsSync } from 'fs';
|
|
6
|
+
import { existsSync, readFileSync } from 'fs';
|
|
7
7
|
|
|
8
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
9
|
const __dirname = dirname(__filename);
|
|
@@ -115,9 +115,17 @@ class YouTrackMCPServer {
|
|
|
115
115
|
* Get server information
|
|
116
116
|
*/
|
|
117
117
|
getInfo() {
|
|
118
|
+
let version = 'unknown';
|
|
119
|
+
try {
|
|
120
|
+
// package.json sits one level up from this file in both src/ and dist/
|
|
121
|
+
const packageJsonPath = join(__dirname, '..', 'package.json');
|
|
122
|
+
version = JSON.parse(readFileSync(packageJsonPath, 'utf8')).version;
|
|
123
|
+
} catch {
|
|
124
|
+
// leave version as 'unknown' if package.json can't be read
|
|
125
|
+
}
|
|
118
126
|
return {
|
|
119
127
|
name: 'YouTrack MCP Server',
|
|
120
|
-
version
|
|
128
|
+
version,
|
|
121
129
|
description: 'A Model Context Protocol server for JetBrains YouTrack',
|
|
122
130
|
pythonPath: this.pythonPath,
|
|
123
131
|
serverPath: this.serverPath,
|
|
@@ -266,6 +266,7 @@ class IssuesClient:
|
|
|
266
266
|
issue_id: str,
|
|
267
267
|
summary: Optional[str] = None,
|
|
268
268
|
description: Optional[str] = None,
|
|
269
|
+
uses_markdown: Optional[bool] = None,
|
|
269
270
|
additional_fields: Optional[Dict[str, Any]] = None,
|
|
270
271
|
) -> Issue:
|
|
271
272
|
"""
|
|
@@ -275,6 +276,9 @@ class IssuesClient:
|
|
|
275
276
|
issue_id: The issue ID or readable ID
|
|
276
277
|
summary: The new issue summary
|
|
277
278
|
description: The new issue description
|
|
279
|
+
uses_markdown: Whether the description is rendered as Markdown
|
|
280
|
+
(True) or YouTrack Wiki markup (False). Leave as None to keep
|
|
281
|
+
the issue's current setting.
|
|
278
282
|
additional_fields: Additional fields to update
|
|
279
283
|
|
|
280
284
|
Returns:
|
|
@@ -288,6 +292,9 @@ class IssuesClient:
|
|
|
288
292
|
if description is not None:
|
|
289
293
|
data["description"] = description
|
|
290
294
|
|
|
295
|
+
if uses_markdown is not None:
|
|
296
|
+
data["usesMarkdown"] = uses_markdown
|
|
297
|
+
|
|
291
298
|
if additional_fields:
|
|
292
299
|
data.update(additional_fields)
|
|
293
300
|
|
|
@@ -224,6 +224,7 @@ class BasicOperations:
|
|
|
224
224
|
issue_id: str,
|
|
225
225
|
summary: Optional[str] = None,
|
|
226
226
|
description: Optional[str] = None,
|
|
227
|
+
uses_markdown: Optional[bool] = None,
|
|
227
228
|
additional_fields: Optional[Dict[str, Any]] = None,
|
|
228
229
|
) -> str:
|
|
229
230
|
"""
|
|
@@ -235,6 +236,9 @@ class BasicOperations:
|
|
|
235
236
|
issue_id: The issue identifier (e.g., "DEMO-123", "PROJECT-456")
|
|
236
237
|
summary: The new issue summary/title (optional)
|
|
237
238
|
description: The new issue description (optional)
|
|
239
|
+
uses_markdown: Whether the description is rendered as Markdown
|
|
240
|
+
(True) or YouTrack Wiki markup (False); leave as None to keep
|
|
241
|
+
the current setting (optional)
|
|
238
242
|
additional_fields: Additional fields to update as dict (optional)
|
|
239
243
|
|
|
240
244
|
Returns:
|
|
@@ -245,6 +249,7 @@ class BasicOperations:
|
|
|
245
249
|
issue_id=issue_id,
|
|
246
250
|
summary=summary,
|
|
247
251
|
description=description,
|
|
252
|
+
uses_markdown=uses_markdown,
|
|
248
253
|
additional_fields=additional_fields,
|
|
249
254
|
)
|
|
250
255
|
# Convert Issue object to dict if needed
|
|
@@ -309,6 +314,7 @@ class BasicOperations:
|
|
|
309
314
|
"issue_id": "Issue identifier like 'DEMO-123' or 'PROJECT-456'",
|
|
310
315
|
"summary": "New issue summary/title (optional)",
|
|
311
316
|
"description": "New issue description (optional)",
|
|
317
|
+
"uses_markdown": "Set True to render the description as Markdown, False for YouTrack Wiki markup; omit to keep the current setting (optional)",
|
|
312
318
|
"additional_fields": "Additional fields to update as dictionary (optional)"
|
|
313
319
|
}
|
|
314
320
|
},
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -1,411 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
YouTrack Project MCP tools.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
import logging
|
|
7
|
-
from typing import Any, Dict, List, Optional, Union
|
|
8
|
-
|
|
9
|
-
from youtrack_mcp.api.client import YouTrackClient
|
|
10
|
-
from youtrack_mcp.api.issues import IssuesClient
|
|
11
|
-
from youtrack_mcp.api.projects import ProjectsClient
|
|
12
|
-
from youtrack_mcp.mcp_wrappers import sync_wrapper
|
|
13
|
-
|
|
14
|
-
logger = logging.getLogger(__name__)
|
|
15
|
-
|
|
16
|
-
class ProjectTools:
|
|
17
|
-
"""Project-related MCP tools."""
|
|
18
|
-
|
|
19
|
-
def __init__(self):
|
|
20
|
-
"""Initialize the project tools."""
|
|
21
|
-
self.client = YouTrackClient()
|
|
22
|
-
self.projects_api = ProjectsClient(self.client)
|
|
23
|
-
|
|
24
|
-
# Also initialize the issues API for fetching issue details
|
|
25
|
-
self.issues_api = IssuesClient(self.client)
|
|
26
|
-
|
|
27
|
-
@sync_wrapper
|
|
28
|
-
def get_projects(self, include_archived: bool = False) -> str:
|
|
29
|
-
"""
|
|
30
|
-
Get a list of all projects.
|
|
31
|
-
|
|
32
|
-
FORMAT: get_projects(include_archived=False)
|
|
33
|
-
|
|
34
|
-
Args:
|
|
35
|
-
include_archived: Whether to include archived projects
|
|
36
|
-
|
|
37
|
-
Returns:
|
|
38
|
-
JSON string with projects information
|
|
39
|
-
"""
|
|
40
|
-
try:
|
|
41
|
-
projects = self.projects_api.get_projects(include_archived=include_archived)
|
|
42
|
-
|
|
43
|
-
# Handle both Pydantic models and dictionaries in the response
|
|
44
|
-
result = []
|
|
45
|
-
for project in projects:
|
|
46
|
-
if hasattr(project, 'model_dump'):
|
|
47
|
-
result.append(project.model_dump())
|
|
48
|
-
else:
|
|
49
|
-
result.append(project) # Assume it's already a dict
|
|
50
|
-
|
|
51
|
-
return json.dumps(result, indent=2)
|
|
52
|
-
except Exception as e:
|
|
53
|
-
logger.exception("Error getting projects")
|
|
54
|
-
return json.dumps({"error": str(e)})
|
|
55
|
-
|
|
56
|
-
@sync_wrapper
|
|
57
|
-
def get_project(self, project_id: str = None, project: str = None) -> str:
|
|
58
|
-
"""
|
|
59
|
-
Get information about a specific project.
|
|
60
|
-
|
|
61
|
-
FORMAT: get_project(project_id="0-0")
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
project_id: The project ID
|
|
65
|
-
project: Alternative parameter name for project_id
|
|
66
|
-
|
|
67
|
-
Returns:
|
|
68
|
-
JSON string with project information
|
|
69
|
-
"""
|
|
70
|
-
try:
|
|
71
|
-
# Use either project_id or project parameter
|
|
72
|
-
project_identifier = project_id or project
|
|
73
|
-
if not project_identifier:
|
|
74
|
-
return json.dumps({"error": "Project ID is required"})
|
|
75
|
-
|
|
76
|
-
project_obj = self.projects_api.get_project(project_identifier)
|
|
77
|
-
|
|
78
|
-
# Handle both Pydantic models and dictionaries in the response
|
|
79
|
-
if hasattr(project_obj, 'model_dump'):
|
|
80
|
-
result = project_obj.model_dump()
|
|
81
|
-
else:
|
|
82
|
-
result = project_obj # Assume it's already a dict
|
|
83
|
-
|
|
84
|
-
return json.dumps(result, indent=2)
|
|
85
|
-
except Exception as e:
|
|
86
|
-
logger.exception(f"Error getting project {project_id or project}")
|
|
87
|
-
return json.dumps({"error": str(e)})
|
|
88
|
-
|
|
89
|
-
@sync_wrapper
|
|
90
|
-
def get_project_by_name(self, project_name: str) -> str:
|
|
91
|
-
"""
|
|
92
|
-
Find a project by its name.
|
|
93
|
-
|
|
94
|
-
FORMAT: get_project_by_name(project_name="DEMO")
|
|
95
|
-
|
|
96
|
-
Args:
|
|
97
|
-
project_name: The project name or short name
|
|
98
|
-
|
|
99
|
-
Returns:
|
|
100
|
-
JSON string with project information
|
|
101
|
-
"""
|
|
102
|
-
try:
|
|
103
|
-
project = self.projects_api.get_project_by_name(project_name)
|
|
104
|
-
if project:
|
|
105
|
-
# Handle both Pydantic models and dictionaries in the response
|
|
106
|
-
if hasattr(project, 'model_dump'):
|
|
107
|
-
result = project.model_dump()
|
|
108
|
-
else:
|
|
109
|
-
result = project # Assume it's already a dict
|
|
110
|
-
|
|
111
|
-
return json.dumps(result, indent=2)
|
|
112
|
-
else:
|
|
113
|
-
return json.dumps({"error": f"Project '{project_name}' not found"})
|
|
114
|
-
except Exception as e:
|
|
115
|
-
logger.exception(f"Error finding project by name {project_name}")
|
|
116
|
-
return json.dumps({"error": str(e)})
|
|
117
|
-
|
|
118
|
-
@sync_wrapper
|
|
119
|
-
def get_project_issues(self, project_id: str = None, project: str = None, limit: int = 50) -> str:
|
|
120
|
-
"""
|
|
121
|
-
Get issues for a specific project.
|
|
122
|
-
|
|
123
|
-
FORMAT: get_project_issues(project_id="0-0", limit=10)
|
|
124
|
-
|
|
125
|
-
Args:
|
|
126
|
-
project_id: The project ID (e.g., '0-0')
|
|
127
|
-
project: Alternative parameter name for project_id
|
|
128
|
-
limit: Maximum number of issues to return (default: 50)
|
|
129
|
-
|
|
130
|
-
Returns:
|
|
131
|
-
JSON string with the issues
|
|
132
|
-
"""
|
|
133
|
-
try:
|
|
134
|
-
# Use either project_id or project parameter
|
|
135
|
-
project_identifier = project_id or project
|
|
136
|
-
if not project_identifier:
|
|
137
|
-
return json.dumps({"error": "Project ID is required"})
|
|
138
|
-
|
|
139
|
-
# First try with the direct project ID
|
|
140
|
-
try:
|
|
141
|
-
issues = self.projects_api.get_project_issues(project_identifier, limit)
|
|
142
|
-
if issues:
|
|
143
|
-
return json.dumps(issues, indent=2)
|
|
144
|
-
except Exception as e:
|
|
145
|
-
# If that fails, check if it was a non-ID format error
|
|
146
|
-
if not str(e).startswith("Project not found"):
|
|
147
|
-
logger.exception(f"Error getting issues for project {project_identifier}")
|
|
148
|
-
return json.dumps({"error": str(e)})
|
|
149
|
-
|
|
150
|
-
# If that failed, try to find project by name
|
|
151
|
-
try:
|
|
152
|
-
project_obj = self.projects_api.get_project_by_name(project_identifier)
|
|
153
|
-
if project_obj:
|
|
154
|
-
issues = self.projects_api.get_project_issues(project_obj.id, limit)
|
|
155
|
-
return json.dumps(issues, indent=2)
|
|
156
|
-
else:
|
|
157
|
-
return json.dumps({"error": f"Project not found: {project_identifier}"})
|
|
158
|
-
except Exception as e:
|
|
159
|
-
logger.exception(f"Error getting issues for project {project_identifier}")
|
|
160
|
-
return json.dumps({"error": str(e)})
|
|
161
|
-
except Exception as e:
|
|
162
|
-
logger.exception(f"Error processing get_project_issues({project_id or project}, {limit})")
|
|
163
|
-
return json.dumps({"error": str(e)})
|
|
164
|
-
|
|
165
|
-
@sync_wrapper
|
|
166
|
-
def get_custom_fields(self, project_id: str = None, project: str = None) -> str:
|
|
167
|
-
"""
|
|
168
|
-
Get custom fields for a project.
|
|
169
|
-
|
|
170
|
-
FORMAT: get_custom_fields(project_id="0-0")
|
|
171
|
-
|
|
172
|
-
Args:
|
|
173
|
-
project_id: The project ID
|
|
174
|
-
project: Alternative parameter name for project_id
|
|
175
|
-
|
|
176
|
-
Returns:
|
|
177
|
-
JSON string with custom fields information
|
|
178
|
-
"""
|
|
179
|
-
try:
|
|
180
|
-
# Use either project_id or project parameter
|
|
181
|
-
project_identifier = project_id or project
|
|
182
|
-
if not project_identifier:
|
|
183
|
-
return json.dumps({"error": "Project ID is required"})
|
|
184
|
-
|
|
185
|
-
fields = self.projects_api.get_custom_fields(project_identifier)
|
|
186
|
-
|
|
187
|
-
# Handle various response formats safely
|
|
188
|
-
if fields is None:
|
|
189
|
-
return json.dumps([])
|
|
190
|
-
|
|
191
|
-
# If it's a dictionary (direct API response)
|
|
192
|
-
if isinstance(fields, dict):
|
|
193
|
-
return json.dumps(fields, indent=2)
|
|
194
|
-
|
|
195
|
-
# If it's a list of objects
|
|
196
|
-
try:
|
|
197
|
-
result = []
|
|
198
|
-
# Try to iterate, but handle errors safely
|
|
199
|
-
for field in fields:
|
|
200
|
-
if hasattr(field, 'model_dump'):
|
|
201
|
-
result.append(field.model_dump())
|
|
202
|
-
elif isinstance(field, dict):
|
|
203
|
-
result.append(field)
|
|
204
|
-
else:
|
|
205
|
-
# Last resort: convert to string
|
|
206
|
-
result.append(str(field))
|
|
207
|
-
return json.dumps(result, indent=2)
|
|
208
|
-
except Exception as e:
|
|
209
|
-
# If we can't iterate, return the raw string representation
|
|
210
|
-
logger.warning(f"Could not process custom fields response: {str(e)}")
|
|
211
|
-
return json.dumps({"custom_fields": str(fields)})
|
|
212
|
-
except Exception as e:
|
|
213
|
-
logger.exception(f"Error getting custom fields for project {project_id or project}")
|
|
214
|
-
return json.dumps({"error": str(e)})
|
|
215
|
-
|
|
216
|
-
@sync_wrapper
|
|
217
|
-
def create_project(self, name: str, short_name: str, lead_id: str, description: Optional[str] = None) -> str:
|
|
218
|
-
"""
|
|
219
|
-
Create a new project with a required leader.
|
|
220
|
-
|
|
221
|
-
FORMAT: create_project(name="Project Name", short_name="PROJ", lead_id="1-1", description="Description")
|
|
222
|
-
|
|
223
|
-
Args:
|
|
224
|
-
name: The name of the project
|
|
225
|
-
short_name: The short name of the project (used as prefix for issues)
|
|
226
|
-
lead_id: The ID of the user who will be the project leader
|
|
227
|
-
description: The project description (optional)
|
|
228
|
-
|
|
229
|
-
Returns:
|
|
230
|
-
JSON string with the created project information
|
|
231
|
-
"""
|
|
232
|
-
try:
|
|
233
|
-
# Check for missing required parameters
|
|
234
|
-
if not name:
|
|
235
|
-
return json.dumps({"error": "Project name is required"})
|
|
236
|
-
if not short_name:
|
|
237
|
-
return json.dumps({"error": "Project short name is required"})
|
|
238
|
-
if not lead_id:
|
|
239
|
-
return json.dumps({"error": "Project leader ID is required"})
|
|
240
|
-
|
|
241
|
-
project = self.projects_api.create_project(
|
|
242
|
-
name=name,
|
|
243
|
-
short_name=short_name,
|
|
244
|
-
lead_id=lead_id,
|
|
245
|
-
description=description
|
|
246
|
-
)
|
|
247
|
-
|
|
248
|
-
# Handle both Pydantic models and dictionaries in the response
|
|
249
|
-
if hasattr(project, 'model_dump'):
|
|
250
|
-
result = project.model_dump()
|
|
251
|
-
else:
|
|
252
|
-
result = project # Assume it's already a dict
|
|
253
|
-
|
|
254
|
-
return json.dumps(result, indent=2)
|
|
255
|
-
except Exception as e:
|
|
256
|
-
logger.exception(f"Error creating project {name}")
|
|
257
|
-
return json.dumps({"error": str(e)})
|
|
258
|
-
|
|
259
|
-
@sync_wrapper
|
|
260
|
-
def update_project(self, project_id: str = None, project: str = None, name: Optional[str] = None,
|
|
261
|
-
description: Optional[str] = None, archived: Optional[bool] = None,
|
|
262
|
-
lead_id: Optional[str] = None, short_name: Optional[str] = None) -> str:
|
|
263
|
-
"""
|
|
264
|
-
Update an existing project.
|
|
265
|
-
|
|
266
|
-
FORMAT: update_project(project_id="0-0", name="New Name", description="New Description", archived=False, lead_id="1-1", short_name="NEWKEY")
|
|
267
|
-
|
|
268
|
-
Args:
|
|
269
|
-
project_id: The project ID to update
|
|
270
|
-
project: Alternative parameter name for project_id
|
|
271
|
-
name: The new name for the project (optional)
|
|
272
|
-
description: The new project description (optional)
|
|
273
|
-
archived: Whether the project should be archived (optional)
|
|
274
|
-
lead_id: The ID of the new project leader (optional)
|
|
275
|
-
short_name: The new short name for the project (optional) - used as prefix for issue IDs
|
|
276
|
-
|
|
277
|
-
Returns:
|
|
278
|
-
JSON string with the updated project information
|
|
279
|
-
"""
|
|
280
|
-
try:
|
|
281
|
-
# Use either project_id or project parameter
|
|
282
|
-
project_identifier = project_id or project
|
|
283
|
-
if not project_identifier:
|
|
284
|
-
return json.dumps({"error": "Project ID is required"})
|
|
285
|
-
|
|
286
|
-
# First, get the existing project to maintain required fields
|
|
287
|
-
try:
|
|
288
|
-
existing_project = self.projects_api.get_project(project_identifier)
|
|
289
|
-
logger.info(f"Found existing project: {existing_project.name} ({existing_project.id})")
|
|
290
|
-
|
|
291
|
-
# Prepare data for direct API call
|
|
292
|
-
data = {}
|
|
293
|
-
|
|
294
|
-
# Only include parameters that were explicitly provided
|
|
295
|
-
if name is not None:
|
|
296
|
-
data["name"] = name
|
|
297
|
-
if description is not None:
|
|
298
|
-
data["description"] = description
|
|
299
|
-
if archived is not None:
|
|
300
|
-
data["archived"] = archived
|
|
301
|
-
if lead_id is not None:
|
|
302
|
-
data["leader"] = {"id": lead_id}
|
|
303
|
-
if short_name is not None:
|
|
304
|
-
data["shortName"] = short_name
|
|
305
|
-
|
|
306
|
-
# If no parameters were provided, return current project
|
|
307
|
-
if not data:
|
|
308
|
-
logger.info("No parameters to update, returning current project")
|
|
309
|
-
if hasattr(existing_project, 'model_dump'):
|
|
310
|
-
return json.dumps(existing_project.model_dump(), indent=2)
|
|
311
|
-
else:
|
|
312
|
-
return json.dumps(existing_project, indent=2)
|
|
313
|
-
|
|
314
|
-
# Log the data being sent
|
|
315
|
-
logger.info(f"Updating project with data: {data}")
|
|
316
|
-
|
|
317
|
-
# Make direct API call
|
|
318
|
-
try:
|
|
319
|
-
# Use the client directly instead of the API method
|
|
320
|
-
self.client.post(f"admin/projects/{project_identifier}", data=data)
|
|
321
|
-
logger.info("Update API call successful")
|
|
322
|
-
except Exception as e:
|
|
323
|
-
logger.warning(f"Update API call error: {str(e)}")
|
|
324
|
-
# Continue anyway as the update might still have worked
|
|
325
|
-
|
|
326
|
-
# Get the updated project data
|
|
327
|
-
try:
|
|
328
|
-
updated_project = self.projects_api.get_project(project_identifier)
|
|
329
|
-
logger.info(f"Retrieved updated project: {updated_project.name}")
|
|
330
|
-
|
|
331
|
-
if hasattr(updated_project, 'model_dump'):
|
|
332
|
-
return json.dumps(updated_project.model_dump(), indent=2)
|
|
333
|
-
else:
|
|
334
|
-
return json.dumps(updated_project, indent=2)
|
|
335
|
-
except Exception as e:
|
|
336
|
-
logger.error(f"Error retrieving updated project: {str(e)}")
|
|
337
|
-
return json.dumps({"error": f"Project was updated but could not retrieve the result: {str(e)}"})
|
|
338
|
-
except Exception as e:
|
|
339
|
-
return json.dumps({"error": f"Could not update project: {str(e)}"})
|
|
340
|
-
except Exception as e:
|
|
341
|
-
logger.exception(f"Error updating project {project_id or project}")
|
|
342
|
-
return json.dumps({"error": str(e)})
|
|
343
|
-
|
|
344
|
-
def close(self) -> None:
|
|
345
|
-
"""Close the API client."""
|
|
346
|
-
self.client.close()
|
|
347
|
-
|
|
348
|
-
def get_tool_definitions(self) -> Dict[str, Dict[str, Any]]:
|
|
349
|
-
"""
|
|
350
|
-
Get the definitions of all project tools.
|
|
351
|
-
|
|
352
|
-
Returns:
|
|
353
|
-
Dictionary mapping tool names to their configuration
|
|
354
|
-
"""
|
|
355
|
-
return {
|
|
356
|
-
"get_projects": {
|
|
357
|
-
"description": "Get a list of all projects.",
|
|
358
|
-
"parameter_descriptions": {
|
|
359
|
-
"include_archived": "Whether to include archived projects (optional, default: false)"
|
|
360
|
-
}
|
|
361
|
-
},
|
|
362
|
-
"get_project": {
|
|
363
|
-
"description": "Get information about a specific project.",
|
|
364
|
-
"parameter_descriptions": {
|
|
365
|
-
"project_id": "The project ID (e.g., '0-0')",
|
|
366
|
-
"project": "Alternative parameter name for project_id"
|
|
367
|
-
}
|
|
368
|
-
},
|
|
369
|
-
"get_project_by_name": {
|
|
370
|
-
"description": "Find a project by its name.",
|
|
371
|
-
"parameter_descriptions": {
|
|
372
|
-
"project_name": "The project name or short name (e.g., 'DEMO')"
|
|
373
|
-
}
|
|
374
|
-
},
|
|
375
|
-
"get_project_issues": {
|
|
376
|
-
"description": "Get issues for a specific project.",
|
|
377
|
-
"parameter_descriptions": {
|
|
378
|
-
"project_id": "The project ID (e.g., '0-0')",
|
|
379
|
-
"project": "Alternative parameter name for project_id",
|
|
380
|
-
"limit": "Maximum number of issues to return (optional, default: 50)"
|
|
381
|
-
}
|
|
382
|
-
},
|
|
383
|
-
"get_custom_fields": {
|
|
384
|
-
"description": "Get custom fields for a project.",
|
|
385
|
-
"parameter_descriptions": {
|
|
386
|
-
"project_id": "The project ID (e.g., '0-0')",
|
|
387
|
-
"project": "Alternative parameter name for project_id"
|
|
388
|
-
}
|
|
389
|
-
},
|
|
390
|
-
"create_project": {
|
|
391
|
-
"description": "Create a new project with a required leader.",
|
|
392
|
-
"parameter_descriptions": {
|
|
393
|
-
"name": "The name of the project",
|
|
394
|
-
"short_name": "The short name of the project (used as prefix for issues)",
|
|
395
|
-
"lead_id": "The ID of the user who will be the project leader",
|
|
396
|
-
"description": "The project description (optional)"
|
|
397
|
-
}
|
|
398
|
-
},
|
|
399
|
-
"update_project": {
|
|
400
|
-
"description": "Update an existing project.",
|
|
401
|
-
"parameter_descriptions": {
|
|
402
|
-
"project_id": "The project ID to update (e.g., '0-0')",
|
|
403
|
-
"project": "Alternative parameter name for project_id",
|
|
404
|
-
"name": "The new name for the project (optional)",
|
|
405
|
-
"description": "The new project description (optional)",
|
|
406
|
-
"archived": "Whether the project should be archived (optional)",
|
|
407
|
-
"lead_id": "The ID of the new project leader (optional)",
|
|
408
|
-
"short_name": "The new short name for the project (optional) - used as prefix for issue IDs"
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
}
|