@dpesch/mantisbt-mcp-server 1.10.3 → 1.11.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/CHANGELOG.md +41 -0
- package/README.de.md +11 -8
- package/README.md +11 -8
- package/dist/config.js +13 -0
- package/dist/index.js +12 -12
- package/dist/tools/files.js +11 -2
- package/dist/tools/issues.js +47 -15
- package/dist/tools/notes.js +15 -10
- package/docs/cookbook.de.md +67 -0
- package/docs/cookbook.md +67 -0
- package/docs/examples.de.md +8 -0
- package/docs/examples.md +8 -0
- package/package.json +1 -1
- package/server.json +2 -2
- package/.gitea/PULL_REQUEST_TEMPLATE.md +0 -17
- package/.gitea/workflows/ci.yml +0 -95
- package/.github/workflows/ci.yml +0 -32
- package/scripts/hooks/pre-push.mjs +0 -220
- package/scripts/init.mjs +0 -95
- package/scripts/record-fixtures.ts +0 -160
- package/tests/cache.test.ts +0 -265
- package/tests/client.test.ts +0 -372
- package/tests/config.test.ts +0 -263
- package/tests/fixtures/get_current_user.json +0 -40
- package/tests/fixtures/get_issue.json +0 -133
- package/tests/fixtures/get_issue_enums.json +0 -67
- package/tests/fixtures/get_issue_fields_sample.json +0 -76
- package/tests/fixtures/get_project_categories.json +0 -157
- package/tests/fixtures/get_project_versions.json +0 -3
- package/tests/fixtures/get_project_versions_with_data.json +0 -52
- package/tests/fixtures/list_issues.json +0 -95
- package/tests/fixtures/list_projects.json +0 -65
- package/tests/helpers/mock-server.ts +0 -166
- package/tests/helpers/search-mocks.ts +0 -84
- package/tests/prompts/prompts.test.ts +0 -242
- package/tests/resources/resources.test.ts +0 -309
- package/tests/search/embedder.test.ts +0 -81
- package/tests/search/highlight.test.ts +0 -129
- package/tests/search/store.test.ts +0 -193
- package/tests/search/sync.test.ts +0 -249
- package/tests/search/tools.test.ts +0 -661
- package/tests/tools/config.test.ts +0 -212
- package/tests/tools/files.test.ts +0 -343
- package/tests/tools/issues.test.ts +0 -1180
- package/tests/tools/metadata.test.ts +0 -509
- package/tests/tools/monitors.test.ts +0 -101
- package/tests/tools/projects.test.ts +0 -338
- package/tests/tools/relationships.test.ts +0 -177
- package/tests/tools/string-coercion.test.ts +0 -317
- package/tests/tools/users.test.ts +0 -62
- package/tests/utils/date-filter.test.ts +0 -169
- package/tests/version-hint.test.ts +0 -230
- package/tsconfig.build.json +0 -8
- package/vitest.config.ts +0 -8
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,47 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## [1.11.0] – 2026-07-03
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `create_issue` and `update_issue` now accept `custom_fields` (`[{field: {id|name}, value}]`) to write custom field values — reading was already supported via `select`.
|
|
19
|
+
- `update_issue` gained an optional `note` parameter (plus `note_view_state`) that appends a note after a successful update — e.g. the reason for a status change in a single call. If the note fails after a successful PATCH, the response contains `note_error` instead of failing the whole call. `dry_run` previews the note as `would_add_note`.
|
|
20
|
+
- `get_issue` now supports the `select` parameter for server-side field projection (same as `list_issues`), significantly reducing response size for large issues.
|
|
21
|
+
|
|
22
|
+
Feature ideas inspired by [kfnzero/mantis-mcp-server#8](https://github.com/kfnzero/mantis-mcp-server/pull/8).
|
|
23
|
+
|
|
24
|
+
### Security
|
|
25
|
+
|
|
26
|
+
- **HTTP transport now requires `MCP_HTTP_TOKEN`.** Starting with `TRANSPORT=http` and no token set previously exposed all tools (including writes like `create_issue`, `delete_issue`, `upload_file`) unauthenticated to any process that could reach the port. The server now refuses to start in HTTP mode without a token. **Behaviour change for HTTP users:** set `MCP_HTTP_TOKEN` to a secret value. The stdio transport (the default) is unaffected.
|
|
27
|
+
- **`upload_file`'s `file_path` is now disabled over the HTTP transport** unless `MANTIS_UPLOAD_DIR` is configured. Over HTTP, `file_path` reads from the *server's* filesystem — a remote client could otherwise exfiltrate arbitrary server-readable files (e.g. SSH keys) as issue attachments. HTTP clients should upload via the `content` (Base64) parameter. The stdio transport keeps unrestricted `file_path` access (the caller is the trusted local agent); set `MANTIS_UPLOAD_DIR` there too if you want to sandbox it.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## [1.10.6] – 2026-06-29
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- Restored automatic npm publishing via Gitea CI pipeline (triggered on version tags). The manual `--provenance` approach was reverted as Sigstore local signing is not supported outside CI environments with OIDC providers.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## [1.10.5] – 2026-06-29
|
|
40
|
+
|
|
41
|
+
### Security
|
|
42
|
+
|
|
43
|
+
- Fixed `.npmignore` to explicitly exclude sensitive files (`.env*`, `.mcpregistry_*`) and dev artifacts (`src/`, `tests/`, `scripts/`, `.claude/`, `.gitea/`, `.github/`, `.idea/`). Previously, when `.npmignore` exists, npm ignores `.gitignore` entirely — requiring all exclusions to be duplicated. v1.10.4 was unpublished due to accidental inclusion of local credential files; this patch corrects the root cause.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## [1.10.4] – 2026-06-29
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- npm releases are now published with `--provenance`, creating a Sigstore attestation that cryptographically links each package to its source commit. Verifiable with `npm audit signatures @dpesch/mantisbt-mcp-server`.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
14
55
|
## [1.10.3] – 2026-05-15
|
|
15
56
|
|
|
16
57
|
### Fixed
|
package/README.de.md
CHANGED
|
@@ -73,13 +73,13 @@ npm run build
|
|
|
73
73
|
| `TRANSPORT` | – | `stdio` | Transport-Modus: `stdio` oder `http` |
|
|
74
74
|
| `PORT` | – | `3000` | Port für HTTP-Modus |
|
|
75
75
|
| `MCP_HTTP_HOST` | – | `127.0.0.1` | Bind-Adresse für HTTP-Modus. **Geändert von `0.0.0.0` auf `127.0.0.1`** — der Server horcht standardmäßig nur auf localhost. Für Docker oder Remote-Zugriff `0.0.0.0` setzen. |
|
|
76
|
-
| `MCP_HTTP_TOKEN` |
|
|
76
|
+
| `MCP_HTTP_TOKEN` | ✅ (HTTP-Modus) | – | Bearer-Token für den `/mcp`-Endpunkt (`Authorization: Bearer <token>`). **Pflicht bei `TRANSPORT=http`** — der Server startet im HTTP-Modus ohne diesen Wert nicht, damit Tools nie unauthentifiziert erreichbar sind. Im stdio-Modus ohne Bedeutung. `/health` ist immer öffentlich. |
|
|
77
77
|
| `MANTIS_SEARCH_ENABLED` | – | `false` | Auf `true` setzen, um die semantische Suche zu aktivieren |
|
|
78
78
|
| `MANTIS_SEARCH_BACKEND` | – | `vectra` | Vektorspeicher: `vectra` (reines JS) oder `sqlite-vec` (manuelle Installation erforderlich) |
|
|
79
79
|
| `MANTIS_SEARCH_DIR` | – | `{MANTIS_CACHE_DIR}/search` | Verzeichnis für den Suchindex |
|
|
80
80
|
| `MANTIS_SEARCH_MODEL` | – | `Xenova/paraphrase-multilingual-MiniLM-L12-v2` | Embedding-Modell (wird beim ersten Start einmalig heruntergeladen, ~80 MB) |
|
|
81
81
|
| `MANTIS_SEARCH_THREADS` | – | `1` | Anzahl der ONNX-Intra-Op-Threads für das Embedding-Modell. Standard ist 1, um CPU-Sättigung auf Mehrkernsystemen und in WSL zu verhindern. Nur erhöhen, wenn die Indexierungsgeschwindigkeit kritisch ist und der Host ausschließlich für diese Last vorgesehen ist. |
|
|
82
|
-
| `MANTIS_UPLOAD_DIR` | – | – | Schränkt `upload_file` auf
|
|
82
|
+
| `MANTIS_UPLOAD_DIR` | – | – | Schränkt den `file_path` von `upload_file` auf dieses Verzeichnis ein (Pfad-Traversal via `../` wird blockiert). Im **stdio-Modus** ist `file_path` ohne diese Variable unbeschränkt. Im **HTTP-Modus** liest `file_path` aus dem Dateisystem des Servers und ist deshalb **deaktiviert, sofern diese Variable nicht gesetzt ist** — HTTP-Clients sollten stattdessen den Parameter `content` (Base64) verwenden. |
|
|
83
83
|
|
|
84
84
|
## Verfügbare Tools
|
|
85
85
|
|
|
@@ -87,11 +87,11 @@ npm run build
|
|
|
87
87
|
|
|
88
88
|
| Tool | Beschreibung |
|
|
89
89
|
|---|---|
|
|
90
|
-
| `get_issue` | Ein Issue anhand seiner ID abrufen |
|
|
90
|
+
| `get_issue` | Ein Issue anhand seiner ID abrufen; optionales `select` zur Feldauswahl reduziert die Antwortgröße |
|
|
91
91
|
| `get_issues` | Mehrere Issues per ID in einem Aufruf abrufen (1–50 IDs); nicht zugängliche IDs liefern `null` an ihrer Position, statt den gesamten Aufruf abzubrechen |
|
|
92
92
|
| `list_issues` | Issues nach Projekt, Status, Autor u.v.m. filtern; optionales `select` für Feldprojektion und `status` für clientseitige Statusfilterung — kanonische englische Statusnamen (z.B. `"new"`, `"resolved"`) werden per ID abgeglichen und funktionieren damit sprachunabhängig auf lokalisierten Installationen |
|
|
93
|
-
| `create_issue` | Neues Issue anlegen; `severity` und `priority` müssen kanonische englische Namen sein (z.B. `minor`, `major`, `normal`, `high`) — `get_issue_enums` aufrufen, um alle gültigen Werte und deren lokalisierte Bezeichnungen zu sehen; optionaler `handler`-Parameter akzeptiert einen Benutzernamen als Alternative zu `handler_id` (wird gegen die Projektmitglieder aufgelöst) |
|
|
94
|
-
| `update_issue` | Bestehendes Issue bearbeiten; Enum-Felder (`status`, `priority`, `severity`, `resolution`, `reproducibility`) akzeptieren kanonische englische Namen, lokalisierte Namen oder numerische IDs — der Server löst Namen automatisch zu IDs auf |
|
|
93
|
+
| `create_issue` | Neues Issue anlegen; `severity` und `priority` müssen kanonische englische Namen sein (z.B. `minor`, `major`, `normal`, `high`) — `get_issue_enums` aufrufen, um alle gültigen Werte und deren lokalisierte Bezeichnungen zu sehen; optionaler `handler`-Parameter akzeptiert einen Benutzernamen als Alternative zu `handler_id` (wird gegen die Projektmitglieder aufgelöst); optionales `custom_fields` zum Setzen benutzerdefinierter Felder |
|
|
94
|
+
| `update_issue` | Bestehendes Issue bearbeiten; Enum-Felder (`status`, `priority`, `severity`, `resolution`, `reproducibility`) akzeptieren kanonische englische Namen, lokalisierte Namen oder numerische IDs — der Server löst Namen automatisch zu IDs auf; unterstützt `custom_fields` sowie einen optionalen `note`-Parameter, der im selben Aufruf eine Notiz anhängt (z.B. die Begründung eines Status-Wechsels) |
|
|
95
95
|
| `delete_issue` | Issue löschen |
|
|
96
96
|
|
|
97
97
|
### Notizen
|
|
@@ -215,16 +215,19 @@ MCP-Prompt-Templates sind Gesprächseinstiege, die den LLM anweisen, strukturier
|
|
|
215
215
|
|
|
216
216
|
## HTTP-Modus
|
|
217
217
|
|
|
218
|
-
Für den Einsatz als eigenständiger Server (z.B. in Remote-Setups):
|
|
218
|
+
Für den Einsatz als eigenständiger Server (z.B. in Remote-Setups). `MCP_HTTP_TOKEN` ist im HTTP-Modus **Pflicht** — der Server startet ohne diesen Wert nicht:
|
|
219
219
|
|
|
220
220
|
```bash
|
|
221
|
-
MANTIS_BASE_URL=... MANTIS_API_KEY=...
|
|
221
|
+
MCP_HTTP_TOKEN=secret MANTIS_BASE_URL=... MANTIS_API_KEY=... \
|
|
222
|
+
TRANSPORT=http PORT=3456 node dist/index.js
|
|
222
223
|
|
|
223
|
-
# Mit
|
|
224
|
+
# Mit expliziter Bind-Adresse (erforderlich für Docker/Remote):
|
|
224
225
|
# MCP_HTTP_TOKEN=secret MANTIS_BASE_URL=... MANTIS_API_KEY=... \
|
|
225
226
|
# TRANSPORT=http PORT=3456 MCP_HTTP_HOST=0.0.0.0 node dist/index.js
|
|
226
227
|
```
|
|
227
228
|
|
|
229
|
+
Jede `/mcp`-Anfrage muss `Authorization: Bearer <token>` senden. Im HTTP-Modus ist der `file_path` von `upload_file` deaktiviert, sofern `MANTIS_UPLOAD_DIR` nicht gesetzt ist — stattdessen den Parameter `content` (Base64) verwenden.
|
|
230
|
+
|
|
228
231
|
Healthcheck: `GET http://localhost:3456/health` (immer öffentlich, kein Token erforderlich)
|
|
229
232
|
|
|
230
233
|
## Dokumentation
|
package/README.md
CHANGED
|
@@ -73,13 +73,13 @@ npm run build
|
|
|
73
73
|
| `TRANSPORT` | – | `stdio` | Transport mode: `stdio` or `http` |
|
|
74
74
|
| `PORT` | – | `3000` | Port for HTTP mode |
|
|
75
75
|
| `MCP_HTTP_HOST` | – | `127.0.0.1` | Bind address for HTTP mode. **Changed from `0.0.0.0` to `127.0.0.1`** — the server now listens on localhost only by default. Set to `0.0.0.0` for Docker or remote access. |
|
|
76
|
-
| `MCP_HTTP_TOKEN` |
|
|
76
|
+
| `MCP_HTTP_TOKEN` | ✅ (HTTP mode) | – | Bearer token for the `/mcp` endpoint (`Authorization: Bearer <token>`). **Required when `TRANSPORT=http`** — the server refuses to start in HTTP mode without it, so tools are never exposed unauthenticated. Ignored in stdio mode. The `/health` endpoint is always public. |
|
|
77
77
|
| `MANTIS_SEARCH_ENABLED` | – | `false` | Set to `true` to enable semantic search |
|
|
78
78
|
| `MANTIS_SEARCH_BACKEND` | – | `vectra` | Vector store backend: `vectra` (pure JS) or `sqlite-vec` (requires manual install) |
|
|
79
79
|
| `MANTIS_SEARCH_DIR` | – | `{MANTIS_CACHE_DIR}/search` | Directory for the search index |
|
|
80
80
|
| `MANTIS_SEARCH_MODEL` | – | `Xenova/paraphrase-multilingual-MiniLM-L12-v2` | Embedding model name (downloaded once on first use, ~80 MB) |
|
|
81
81
|
| `MANTIS_SEARCH_THREADS` | – | `1` | Number of ONNX intra-op threads for the embedding model. Default is 1 to prevent CPU saturation on multi-core machines and WSL. Increase only if index rebuild speed matters and the host is dedicated to this workload. |
|
|
82
|
-
| `MANTIS_UPLOAD_DIR` | – | – | Restrict `upload_file` to files within this directory.
|
|
82
|
+
| `MANTIS_UPLOAD_DIR` | – | – | Restrict `upload_file`'s `file_path` to files within this directory (path traversal via `../` is blocked). In **stdio mode** `file_path` is unrestricted unless this is set. In **HTTP mode** `file_path` reads from the server's filesystem, so it is **disabled unless this variable is set** — HTTP clients should upload via the `content` (Base64) parameter instead. |
|
|
83
83
|
|
|
84
84
|
## Available tools
|
|
85
85
|
|
|
@@ -87,11 +87,11 @@ npm run build
|
|
|
87
87
|
|
|
88
88
|
| Tool | Description |
|
|
89
89
|
|---|---|
|
|
90
|
-
| `get_issue` | Retrieve an issue by its numeric ID |
|
|
90
|
+
| `get_issue` | Retrieve an issue by its numeric ID; optional `select` for field projection to reduce response size |
|
|
91
91
|
| `get_issues` | Retrieve multiple issues by ID in one call (1–50 IDs); missing or inaccessible IDs return `null` at their position instead of failing the call |
|
|
92
92
|
| `list_issues` | Filter issues by project, status, author, and more; optional `select` for field projection and `status` for client-side status filtering — canonical English status names (e.g. `"new"`, `"resolved"`) are matched by ID, making the filter language-independent on localized installations |
|
|
93
|
-
| `create_issue` | Create a new issue; `severity` and `priority` must be canonical English names (e.g. `minor`, `major`, `normal`, `high`) — call `get_issue_enums` to see all valid values and their localized labels; optional `handler` parameter accepts a username as alternative to `handler_id` (resolved against project members) |
|
|
94
|
-
| `update_issue` | Update an existing issue; enum fields (`status`, `priority`, `severity`, `resolution`, `reproducibility`) accept canonical English names, localized names, or numeric IDs — the server resolves names to IDs automatically |
|
|
93
|
+
| `create_issue` | Create a new issue; `severity` and `priority` must be canonical English names (e.g. `minor`, `major`, `normal`, `high`) — call `get_issue_enums` to see all valid values and their localized labels; optional `handler` parameter accepts a username as alternative to `handler_id` (resolved against project members); optional `custom_fields` to set custom field values |
|
|
94
|
+
| `update_issue` | Update an existing issue; enum fields (`status`, `priority`, `severity`, `resolution`, `reproducibility`) accept canonical English names, localized names, or numeric IDs — the server resolves names to IDs automatically; supports `custom_fields` and an optional `note` parameter that appends a note in the same call (e.g. the reason for a status change) |
|
|
95
95
|
| `delete_issue` | Delete an issue |
|
|
96
96
|
|
|
97
97
|
### Notes
|
|
@@ -215,16 +215,19 @@ MCP prompt templates are conversation starters that instruct the LLM to collect
|
|
|
215
215
|
|
|
216
216
|
## HTTP mode
|
|
217
217
|
|
|
218
|
-
For use as a standalone server (e.g. in remote setups):
|
|
218
|
+
For use as a standalone server (e.g. in remote setups). `MCP_HTTP_TOKEN` is **required** in HTTP mode — the server refuses to start without it:
|
|
219
219
|
|
|
220
220
|
```bash
|
|
221
|
-
MANTIS_BASE_URL=... MANTIS_API_KEY=...
|
|
221
|
+
MCP_HTTP_TOKEN=secret MANTIS_BASE_URL=... MANTIS_API_KEY=... \
|
|
222
|
+
TRANSPORT=http PORT=3456 node dist/index.js
|
|
222
223
|
|
|
223
|
-
# With
|
|
224
|
+
# With explicit bind address (required for Docker/remote):
|
|
224
225
|
# MCP_HTTP_TOKEN=secret MANTIS_BASE_URL=... MANTIS_API_KEY=... \
|
|
225
226
|
# TRANSPORT=http PORT=3456 MCP_HTTP_HOST=0.0.0.0 node dist/index.js
|
|
226
227
|
```
|
|
227
228
|
|
|
229
|
+
Every `/mcp` request must send `Authorization: Bearer <token>`. Over HTTP, `upload_file`'s `file_path` is disabled unless `MANTIS_UPLOAD_DIR` is set — use the `content` (Base64) parameter instead.
|
|
230
|
+
|
|
228
231
|
Health check: `GET http://localhost:3456/health` (always public, no token required)
|
|
229
232
|
|
|
230
233
|
## Documentation
|
package/dist/config.js
CHANGED
|
@@ -67,6 +67,19 @@ export async function getStartupConfig() {
|
|
|
67
67
|
await loadDotEnvLocal();
|
|
68
68
|
return readNonCredentialConfig();
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Enforces authentication for the HTTP transport. Without a token, every tool
|
|
72
|
+
* (including writes like create_issue, delete_issue and upload_file) would be
|
|
73
|
+
* reachable unauthenticated by any process that can reach the port. Aborts
|
|
74
|
+
* startup when the token is missing or blank so HTTP mode is never exposed open.
|
|
75
|
+
*/
|
|
76
|
+
export function assertHttpAuthConfigured(httpToken) {
|
|
77
|
+
if (httpToken === undefined || httpToken.trim() === '') {
|
|
78
|
+
throw new Error('HTTP transport requires authentication: set MCP_HTTP_TOKEN to a secret value.\n' +
|
|
79
|
+
'Without it, all tools would be exposed unauthenticated. ' +
|
|
80
|
+
'Use the stdio transport (the default) if you do not need HTTP.');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
70
83
|
// ---------------------------------------------------------------------------
|
|
71
84
|
// Full config (credentials required)
|
|
72
85
|
// ---------------------------------------------------------------------------
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { createServer } from 'node:http';
|
|
|
6
6
|
import { readFileSync } from 'node:fs';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { join, dirname } from 'node:path';
|
|
9
|
-
import { getConfig, getStartupConfig } from './config.js';
|
|
9
|
+
import { getConfig, getStartupConfig, assertHttpAuthConfigured } from './config.js';
|
|
10
10
|
import { MantisClient } from './client.js';
|
|
11
11
|
import { VersionHintService, setGlobalVersionHint } from './version-hint.js';
|
|
12
12
|
import { MetadataCache } from './cache.js';
|
|
@@ -34,7 +34,7 @@ const version = packageJson.version;
|
|
|
34
34
|
// ---------------------------------------------------------------------------
|
|
35
35
|
// Bootstrap
|
|
36
36
|
// ---------------------------------------------------------------------------
|
|
37
|
-
async function createMcpServer() {
|
|
37
|
+
async function createMcpServer(transport) {
|
|
38
38
|
// Use startup config (no credentials required) so the server can connect
|
|
39
39
|
// and respond to tools/list even without MANTIS_BASE_URL / MANTIS_API_KEY.
|
|
40
40
|
// Credentials are resolved lazily on the first actual tool invocation.
|
|
@@ -52,7 +52,7 @@ async function createMcpServer() {
|
|
|
52
52
|
});
|
|
53
53
|
registerIssueTools(server, client, cache);
|
|
54
54
|
registerNoteTools(server, client);
|
|
55
|
-
registerFileTools(server, client, startupConfig.uploadDir);
|
|
55
|
+
registerFileTools(server, client, startupConfig.uploadDir, transport);
|
|
56
56
|
registerRelationshipTools(server, client);
|
|
57
57
|
registerMonitorTools(server, client);
|
|
58
58
|
registerProjectTools(server, client, cache);
|
|
@@ -75,7 +75,7 @@ async function createMcpServer() {
|
|
|
75
75
|
// Transport: stdio (default) or HTTP
|
|
76
76
|
// ---------------------------------------------------------------------------
|
|
77
77
|
async function runStdio() {
|
|
78
|
-
const server = await createMcpServer();
|
|
78
|
+
const server = await createMcpServer('stdio');
|
|
79
79
|
const transport = new StdioServerTransport();
|
|
80
80
|
await server.connect(transport);
|
|
81
81
|
console.error(`MantisBT MCP Server v${version} running (stdio)`);
|
|
@@ -86,7 +86,8 @@ async function runStdio() {
|
|
|
86
86
|
}
|
|
87
87
|
async function runHttp() {
|
|
88
88
|
const startupConfig = await getStartupConfig();
|
|
89
|
-
|
|
89
|
+
assertHttpAuthConfigured(startupConfig.httpToken);
|
|
90
|
+
const server = await createMcpServer('http');
|
|
90
91
|
const port = startupConfig.httpPort;
|
|
91
92
|
// Serialize stateless requests: each request waits for the previous transport
|
|
92
93
|
// to be fully closed before connecting a new one. Without this, concurrent
|
|
@@ -96,13 +97,12 @@ async function runHttp() {
|
|
|
96
97
|
let requestQueue = Promise.resolve();
|
|
97
98
|
const httpServer = createServer(async (req, res) => {
|
|
98
99
|
if (req.method === 'POST' && req.url === '/mcp') {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
100
|
+
// Token is guaranteed present by assertHttpAuthConfigured() at startup.
|
|
101
|
+
const auth = req.headers['authorization'];
|
|
102
|
+
if (auth !== `Bearer ${startupConfig.httpToken}`) {
|
|
103
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
104
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
105
|
+
return;
|
|
106
106
|
}
|
|
107
107
|
const chunks = [];
|
|
108
108
|
req.on('data', (chunk) => chunks.push(chunk));
|
package/dist/tools/files.js
CHANGED
|
@@ -8,8 +8,14 @@ function errorText(msg) {
|
|
|
8
8
|
const hint = vh?.getUpdateHint();
|
|
9
9
|
return hint ? `Error: ${msg}\n\n${hint}` : `Error: ${msg}`;
|
|
10
10
|
}
|
|
11
|
-
export function registerFileTools(server, client, uploadDir) {
|
|
11
|
+
export function registerFileTools(server, client, uploadDir, transport = 'stdio') {
|
|
12
12
|
const normalizedUploadDir = uploadDir ? resolve(uploadDir) + sep : undefined;
|
|
13
|
+
// file_path reads from the server's own filesystem. Over stdio the caller is
|
|
14
|
+
// the trusted local agent (same machine, same user), so any path is fine. Over
|
|
15
|
+
// HTTP a remote client neither controls nor should reach server-local paths —
|
|
16
|
+
// it must send Base64 via `content`. Only allow file_path in HTTP mode when an
|
|
17
|
+
// explicit MANTIS_UPLOAD_DIR sandboxes it.
|
|
18
|
+
const filePathAllowed = transport === 'stdio' || normalizedUploadDir !== undefined;
|
|
13
19
|
// ---------------------------------------------------------------------------
|
|
14
20
|
// list_issue_files
|
|
15
21
|
// ---------------------------------------------------------------------------
|
|
@@ -47,7 +53,7 @@ Use this tool when you need to inspect or enumerate files attached to an issue.
|
|
|
47
53
|
description: `Upload a file as an attachment to a MantisBT issue. Adds the file to the issue without modifying any issue fields or status. Returns the created attachment metadata on success.
|
|
48
54
|
|
|
49
55
|
Provide exactly one of the two input modes:
|
|
50
|
-
- file_path (preferred): absolute path to a local file — use this whenever the file exists on disk; the server reads and encodes it automatically; filename is derived from the path
|
|
56
|
+
- file_path (preferred): absolute path to a local file — use this whenever the file exists on disk; the server reads and encodes it automatically; filename is derived from the path. Note: file_path reads from the server's filesystem and is disabled over the HTTP transport unless MANTIS_UPLOAD_DIR is configured — HTTP clients should use content instead.
|
|
51
57
|
- content: Base64-encoded file content — only use this when the file is not accessible via a path (e.g. in-memory data); filename must be supplied explicitly via the filename parameter
|
|
52
58
|
|
|
53
59
|
The optional content_type sets the MIME type (e.g. "image/png"); defaults to "application/octet-stream". Use the optional description to annotate the attachment.
|
|
@@ -77,6 +83,9 @@ Use this tool to attach files such as logs, screenshots, or patches to an existi
|
|
|
77
83
|
let base64Content;
|
|
78
84
|
let fileName;
|
|
79
85
|
if (file_path) {
|
|
86
|
+
if (!filePathAllowed) {
|
|
87
|
+
return { content: [{ type: 'text', text: errorText('file_path is disabled over the HTTP transport. Send the file via the "content" parameter (Base64), or set MANTIS_UPLOAD_DIR to permit server-local paths within that directory.') }], isError: true };
|
|
88
|
+
}
|
|
80
89
|
if (normalizedUploadDir) {
|
|
81
90
|
const normalizedPath = resolve(file_path);
|
|
82
91
|
if (!normalizedPath.startsWith(normalizedUploadDir)) {
|
package/dist/tools/issues.js
CHANGED
|
@@ -4,22 +4,35 @@ import { getVersionHint } from '../version-hint.js';
|
|
|
4
4
|
import { MANTIS_CANONICAL_ENUM_NAMES, MANTIS_RESOLVED_STATUS_ID, resolveEnumId } from '../constants.js';
|
|
5
5
|
import { dateFilterSchema, matchesDateFilter, hasDateFilter } from '../date-filter.js';
|
|
6
6
|
import { fetchIssueEnumsWithCache } from './config.js';
|
|
7
|
+
import { postNote } from './notes.js';
|
|
7
8
|
function errorText(msg) {
|
|
8
9
|
const vh = getVersionHint();
|
|
9
10
|
vh?.triggerLatestVersionFetch();
|
|
10
11
|
const hint = vh?.getUpdateHint();
|
|
11
12
|
return hint ? `Error: ${msg}\n\n${hint}` : `Error: ${msg}`;
|
|
12
13
|
}
|
|
13
|
-
|
|
14
|
+
// issueId: fallback when a select projection excluded the id field from the response
|
|
15
|
+
function enrichIssue(issue, baseUrl, issueId) {
|
|
16
|
+
const id = issue.id ?? issueId;
|
|
17
|
+
if (id === undefined)
|
|
18
|
+
return issue;
|
|
14
19
|
return {
|
|
15
20
|
...issue,
|
|
16
|
-
view_url: buildIssueViewUrl(baseUrl,
|
|
21
|
+
view_url: buildIssueViewUrl(baseUrl, id),
|
|
17
22
|
notes: issue.notes?.map(note => ({
|
|
18
23
|
...note,
|
|
19
|
-
view_url: buildNoteViewUrl(baseUrl,
|
|
24
|
+
view_url: buildNoteViewUrl(baseUrl, id, note.id),
|
|
20
25
|
})),
|
|
21
26
|
};
|
|
22
27
|
}
|
|
28
|
+
// MantisBT reference shape: at least one of id or name must be provided
|
|
29
|
+
const ref = z.object({ id: z.number().optional(), name: z.string().optional() })
|
|
30
|
+
.refine(o => o.id !== undefined || o.name !== undefined, { message: "At least one of 'id' or 'name' must be provided" });
|
|
31
|
+
// Custom field entry in MantisBT REST format: { field: {id|name}, value: "<string>" }
|
|
32
|
+
const customFieldEntry = z.object({
|
|
33
|
+
field: ref.describe('Custom field reference: { id } or { name }'),
|
|
34
|
+
value: z.coerce.string().describe('Field value as string'),
|
|
35
|
+
});
|
|
23
36
|
const GET_ISSUES_CONCURRENCY = 5;
|
|
24
37
|
// Worker-pool: runs `fn` over all `items` with at most `concurrency` in-flight at once.
|
|
25
38
|
// nextIndex is only incremented inside microtasks, so the ++ is safe without a lock.
|
|
@@ -61,22 +74,23 @@ export function registerIssueTools(server, client, cache) {
|
|
|
61
74
|
// ---------------------------------------------------------------------------
|
|
62
75
|
server.registerTool('get_issue', {
|
|
63
76
|
title: 'Get Issue',
|
|
64
|
-
description: 'Retrieve a single MantisBT issue by its numeric ID. Returns all issue fields including notes, attachments, and relationships. Notes are always included — no separate list_notes call needed.',
|
|
77
|
+
description: 'Retrieve a single MantisBT issue by its numeric ID. Returns all issue fields including notes, attachments, and relationships (unless "select" is given). Notes are always included — no separate list_notes call needed.',
|
|
65
78
|
inputSchema: z.object({
|
|
66
79
|
id: z.coerce.number().int().positive().describe('Numeric issue ID'),
|
|
80
|
+
select: z.string().optional().describe('Comma-separated list of fields to include in the response (server-side projection, same as list_issues). Significantly reduces response size. Example: "id,summary,status,notes"'),
|
|
67
81
|
}),
|
|
68
82
|
annotations: {
|
|
69
83
|
readOnlyHint: true,
|
|
70
84
|
destructiveHint: false,
|
|
71
85
|
idempotentHint: true,
|
|
72
86
|
},
|
|
73
|
-
}, async ({ id }) => {
|
|
87
|
+
}, async ({ id, select }) => {
|
|
74
88
|
try {
|
|
75
|
-
const result = await client.get(`issues/${id}
|
|
89
|
+
const result = await client.get(`issues/${id}`, { select });
|
|
76
90
|
const issue = result.issues?.[0] ?? result;
|
|
77
91
|
const baseUrl = await client.getBaseUrl();
|
|
78
92
|
return {
|
|
79
|
-
content: [{ type: 'text', text: JSON.stringify(enrichIssue(issue, baseUrl), null, 2) }],
|
|
93
|
+
content: [{ type: 'text', text: JSON.stringify(enrichIssue(issue, baseUrl, id), null, 2) }],
|
|
80
94
|
};
|
|
81
95
|
}
|
|
82
96
|
catch (error) {
|
|
@@ -278,13 +292,14 @@ For the handler, prefer the username field (resolved server-side) over handler_i
|
|
|
278
292
|
additional_information: z.string().optional().describe('Additional context or notes about the issue. Plain text or Markdown.'),
|
|
279
293
|
reproducibility: z.string().optional().describe('How reliably the issue reproduces. Canonical English names: always, sometimes, random, have not tried, unable to reproduce, N/A. Use get_issue_enums to see localized labels.'),
|
|
280
294
|
view_state: z.enum(['public', 'private']).optional().describe('Visibility of the issue: "public" (visible to all, default) or "private" (restricted to higher-access users).'),
|
|
295
|
+
custom_fields: z.array(customFieldEntry).optional().describe('Custom field values: [{field: {id|name}, value: "<string>"}]. Use get_issue_fields or get_metadata to discover available custom fields per project.'),
|
|
281
296
|
}),
|
|
282
297
|
annotations: {
|
|
283
298
|
readOnlyHint: false,
|
|
284
299
|
destructiveHint: false,
|
|
285
300
|
idempotentHint: false,
|
|
286
301
|
},
|
|
287
|
-
}, async ({ summary, description, project_id, category, priority, severity, handler_id, handler, version, target_version, fixed_in_version, steps_to_reproduce, additional_information, reproducibility, view_state }) => {
|
|
302
|
+
}, async ({ summary, description, project_id, category, priority, severity, handler_id, handler, version, target_version, fixed_in_version, steps_to_reproduce, additional_information, reproducibility, view_state, custom_fields }) => {
|
|
288
303
|
// Resolve handler username to handler_id when only a name is given
|
|
289
304
|
let resolvedHandlerId = handler_id;
|
|
290
305
|
if (resolvedHandlerId === undefined && handler !== undefined) {
|
|
@@ -344,6 +359,8 @@ For the handler, prefer the username field (resolved server-side) over handler_i
|
|
|
344
359
|
}
|
|
345
360
|
if (view_state !== undefined)
|
|
346
361
|
body.view_state = { name: view_state };
|
|
362
|
+
if (custom_fields !== undefined)
|
|
363
|
+
body.custom_fields = custom_fields;
|
|
347
364
|
const raw = await client.post('issues', body);
|
|
348
365
|
const partial = ('issue' in raw && typeof raw['issue'] === 'object' && raw['issue'] !== null)
|
|
349
366
|
? raw['issue']
|
|
@@ -374,9 +391,6 @@ For the handler, prefer the username field (resolved server-side) over handler_i
|
|
|
374
391
|
// update_issue
|
|
375
392
|
// ---------------------------------------------------------------------------
|
|
376
393
|
const coerceBool = (val) => val === 'true' ? true : val === 'false' ? false : val;
|
|
377
|
-
// MantisBT reference shape: at least one of id or name must be provided
|
|
378
|
-
const ref = z.object({ id: z.number().optional(), name: z.string().optional() })
|
|
379
|
-
.refine(o => o.id !== undefined || o.name !== undefined, { message: "At least one of 'id' or 'name' must be provided" });
|
|
380
394
|
server.registerTool('update_issue', {
|
|
381
395
|
title: 'Update Issue',
|
|
382
396
|
description: `Update one or more fields of an existing MantisBT issue using a partial PATCH.
|
|
@@ -397,11 +411,16 @@ The "fields" object accepts any combination of:
|
|
|
397
411
|
- target_version: { name: "<version_name>" }
|
|
398
412
|
- fixed_in_version: { name: "<version_name>" }
|
|
399
413
|
- view_state: { name: "public"|"private" }
|
|
414
|
+
- custom_fields: [{field: {id|name}, value: "<string>"}] (only the listed custom fields are changed, others stay untouched; use get_issue_fields to discover fields)
|
|
400
415
|
|
|
401
|
-
Important: when resolving an issue, always set BOTH status and resolution to avoid leaving resolution as "open"
|
|
416
|
+
Important: when resolving an issue, always set BOTH status and resolution to avoid leaving resolution as "open".
|
|
417
|
+
|
|
418
|
+
Use the optional "note" parameter to append a note in the same call (e.g. the reason for a status change) — no separate add_note call needed. For a note without field changes use add_note.`,
|
|
402
419
|
inputSchema: z.object({
|
|
403
420
|
id: z.coerce.number().int().positive().describe('Numeric issue ID to update'),
|
|
404
421
|
dry_run: z.preprocess(coerceBool, z.boolean().optional()).describe('If true, return the patch payload that would be sent without actually updating the issue. Useful for previewing changes before committing them.'),
|
|
422
|
+
note: z.string().min(1).optional().describe('Optional note text appended after a successful update (e.g. reason for a status change). Replaces a separate add_note call.'),
|
|
423
|
+
note_view_state: z.enum(['public', 'private']).default('public').describe('Visibility of the appended note: "public" (default) or "private". Only used when "note" is set.'),
|
|
405
424
|
fields: z.preprocess((v) => {
|
|
406
425
|
if (typeof v !== 'string')
|
|
407
426
|
return v;
|
|
@@ -427,6 +446,7 @@ Important: when resolving an issue, always set BOTH status and resolution to avo
|
|
|
427
446
|
target_version: ref.optional(),
|
|
428
447
|
fixed_in_version: ref.optional(),
|
|
429
448
|
view_state: ref.optional(),
|
|
449
|
+
custom_fields: z.array(customFieldEntry).optional(),
|
|
430
450
|
}).strict().describe('Fields to update (partial update — only provided fields are changed; unknown keys are rejected)')),
|
|
431
451
|
}),
|
|
432
452
|
annotations: {
|
|
@@ -434,10 +454,10 @@ Important: when resolving an issue, always set BOTH status and resolution to avo
|
|
|
434
454
|
destructiveHint: false,
|
|
435
455
|
idempotentHint: false,
|
|
436
456
|
},
|
|
437
|
-
}, async ({ id, fields, dry_run }) => {
|
|
457
|
+
}, async ({ id, fields, dry_run, note, note_view_state }) => {
|
|
438
458
|
if (dry_run) {
|
|
439
459
|
return {
|
|
440
|
-
content: [{ type: 'text', text: JSON.stringify({ dry_run: true, id, would_patch: fields }, null, 2) }],
|
|
460
|
+
content: [{ type: 'text', text: JSON.stringify({ dry_run: true, id, would_patch: fields, would_add_note: note ?? null }, null, 2) }],
|
|
441
461
|
};
|
|
442
462
|
}
|
|
443
463
|
try {
|
|
@@ -455,8 +475,20 @@ Important: when resolving an issue, always set BOTH status and resolution to avo
|
|
|
455
475
|
client.getBaseUrl(),
|
|
456
476
|
]);
|
|
457
477
|
const issue = result.issue ?? result;
|
|
478
|
+
const response = { ...enrichIssue(issue, baseUrl, id) };
|
|
479
|
+
if (note !== undefined) {
|
|
480
|
+
// The PATCH already succeeded — a note failure must not look like a failed
|
|
481
|
+
// update, or the caller would retry the whole update. Report it inline instead.
|
|
482
|
+
try {
|
|
483
|
+
response['note'] = await postNote(client, id, note, note_view_state);
|
|
484
|
+
}
|
|
485
|
+
catch (noteError) {
|
|
486
|
+
const noteMsg = noteError instanceof Error ? noteError.message : String(noteError);
|
|
487
|
+
response['note_error'] = `Issue #${id} was updated successfully, but adding the note failed: ${noteMsg}. Retry with add_note.`;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
458
490
|
return {
|
|
459
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
491
|
+
content: [{ type: 'text', text: JSON.stringify(response, null, 2) }],
|
|
460
492
|
};
|
|
461
493
|
}
|
|
462
494
|
catch (error) {
|
package/dist/tools/notes.js
CHANGED
|
@@ -7,6 +7,19 @@ function errorText(msg) {
|
|
|
7
7
|
const hint = vh?.getUpdateHint();
|
|
8
8
|
return hint ? `Error: ${msg}\n\n${hint}` : `Error: ${msg}`;
|
|
9
9
|
}
|
|
10
|
+
// Shared with update_issue (note parameter) — posts a note and returns it enriched with view_url.
|
|
11
|
+
export async function postNote(client, issueId, text, viewState) {
|
|
12
|
+
const body = {
|
|
13
|
+
text,
|
|
14
|
+
view_state: { name: viewState },
|
|
15
|
+
};
|
|
16
|
+
const [result, baseUrl] = await Promise.all([
|
|
17
|
+
client.post(`issues/${issueId}/notes`, body),
|
|
18
|
+
client.getBaseUrl(),
|
|
19
|
+
]);
|
|
20
|
+
const note = result.note ?? result;
|
|
21
|
+
return { ...note, view_url: buildNoteViewUrl(baseUrl, issueId, note.id) };
|
|
22
|
+
}
|
|
10
23
|
export function registerNoteTools(server, client) {
|
|
11
24
|
// ---------------------------------------------------------------------------
|
|
12
25
|
// list_notes
|
|
@@ -65,17 +78,9 @@ Prerequisites: obtain issue_id from list_issues, get_issue, or search_issues.`,
|
|
|
65
78
|
},
|
|
66
79
|
}, async ({ issue_id, text, view_state }) => {
|
|
67
80
|
try {
|
|
68
|
-
const
|
|
69
|
-
text,
|
|
70
|
-
view_state: { name: view_state },
|
|
71
|
-
};
|
|
72
|
-
const [result, baseUrl] = await Promise.all([
|
|
73
|
-
client.post(`issues/${issue_id}/notes`, body),
|
|
74
|
-
client.getBaseUrl(),
|
|
75
|
-
]);
|
|
76
|
-
const note = result.note ?? result;
|
|
81
|
+
const note = await postNote(client, issue_id, text, view_state);
|
|
77
82
|
return {
|
|
78
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
83
|
+
content: [{ type: 'text', text: JSON.stringify(note, null, 2) }],
|
|
79
84
|
};
|
|
80
85
|
}
|
|
81
86
|
catch (error) {
|
package/docs/cookbook.de.md
CHANGED
|
@@ -18,6 +18,8 @@ Tool-orientierte Rezepte für den MantisBT MCP Server — jedes Rezept zeigt gen
|
|
|
18
18
|
- [Gespeicherten Filter anwenden](#gespeicherten-filter-anwenden)
|
|
19
19
|
- [Issue erstellen](#issue-erstellen)
|
|
20
20
|
- [Issue schließen (Status + Auflösung)](#issue-schließen-status--auflösung)
|
|
21
|
+
- [Issue auflösen mit Begründungs-Notiz (ein Aufruf)](#issue-auflösen-mit-begründungs-notiz-ein-aufruf)
|
|
22
|
+
- [Benutzerdefinierte Felder setzen](#benutzerdefinierte-felder-setzen)
|
|
21
23
|
- [Issue neu zuweisen](#issue-neu-zuweisen)
|
|
22
24
|
- [Fix-Version setzen](#fix-version-setzen)
|
|
23
25
|
- [Notizen](#notizen)
|
|
@@ -194,6 +196,7 @@ Ruft ein einzelnes Issue anhand seiner numerischen ID ab, inklusive Notizen, Anh
|
|
|
194
196
|
|
|
195
197
|
**Parameter:**
|
|
196
198
|
- `id` — numerische Issue-ID
|
|
199
|
+
- `select` — _(optional)_ kommagetrennte Feldliste (server-seitige Projektion); reduziert die Antwortgröße bei großen Issues deutlich, z.B. `"id,summary,status,notes"`
|
|
197
200
|
|
|
198
201
|
**Request:**
|
|
199
202
|
|
|
@@ -203,6 +206,15 @@ Ruft ein einzelnes Issue anhand seiner numerischen ID ab, inklusive Notizen, Anh
|
|
|
203
206
|
}
|
|
204
207
|
```
|
|
205
208
|
|
|
209
|
+
Kompakte Variante (nur die benötigten Felder):
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
{
|
|
213
|
+
"id": 1042,
|
|
214
|
+
"select": "id,summary,status,handler"
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
206
218
|
**Response:**
|
|
207
219
|
|
|
208
220
|
```json
|
|
@@ -606,6 +618,61 @@ Löst ein Issue auf und schließt es. **Immer beide Felder** `status` und `resol
|
|
|
606
618
|
|
|
607
619
|
---
|
|
608
620
|
|
|
621
|
+
### Issue auflösen mit Begründungs-Notiz (ein Aufruf)
|
|
622
|
+
|
|
623
|
+
Ändert den Status und hängt eine Notiz mit der Begründung an — in einem einzigen `update_issue`-Aufruf, kein separates `add_note` nötig.
|
|
624
|
+
|
|
625
|
+
**Tool:** `update_issue`
|
|
626
|
+
|
|
627
|
+
**Parameter:**
|
|
628
|
+
- `id` — numerische Issue-ID
|
|
629
|
+
- `fields` — zu ändernde Felder
|
|
630
|
+
- `note` — Notiztext, der nach erfolgreichem Update angehängt wird
|
|
631
|
+
- `note_view_state` — _(optional)_ `"public"` (Standard) oder `"private"`
|
|
632
|
+
|
|
633
|
+
**Request:**
|
|
634
|
+
|
|
635
|
+
```json
|
|
636
|
+
{
|
|
637
|
+
"id": 1042,
|
|
638
|
+
"fields": {
|
|
639
|
+
"status": { "name": "resolved" },
|
|
640
|
+
"resolution": { "id": 20 }
|
|
641
|
+
},
|
|
642
|
+
"note": "Behoben in Commit abc123 — der Touch-Event-Handler fehlte unter iOS."
|
|
643
|
+
}
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
**Response:** das aktualisierte Issue plus ein `note`-Objekt mit `view_url`.
|
|
647
|
+
|
|
648
|
+
> **Hinweis:** Schlägt die Notiz nach erfolgreichem Update fehl, enthält die Antwort `note_error` statt eines Fehlers für den Gesamtaufruf — dann mit `add_note` nachholen. Für eine Notiz ohne Feldänderungen direkt `add_note` verwenden.
|
|
649
|
+
|
|
650
|
+
---
|
|
651
|
+
|
|
652
|
+
### Benutzerdefinierte Felder setzen
|
|
653
|
+
|
|
654
|
+
Schreibt Custom-Field-Werte beim Anlegen oder Aktualisieren. Dasselbe `custom_fields`-Format funktioniert bei `create_issue` und `update_issue` (dort innerhalb von `fields`).
|
|
655
|
+
|
|
656
|
+
**Tool:** `update_issue`
|
|
657
|
+
|
|
658
|
+
**Request:**
|
|
659
|
+
|
|
660
|
+
```json
|
|
661
|
+
{
|
|
662
|
+
"id": 1042,
|
|
663
|
+
"fields": {
|
|
664
|
+
"custom_fields": [
|
|
665
|
+
{ "field": { "name": "Kunde" }, "value": "ACME Corp" },
|
|
666
|
+
{ "field": { "id": 3 }, "value": "2026-07-01" }
|
|
667
|
+
]
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
> **Hinweis:** Nur die aufgeführten Custom Fields werden geändert — alle anderen bleiben unangetastet. Verfügbare Felder pro Projekt mit `get_issue_fields()` oder `get_metadata()` ermitteln. Werte sind immer Strings.
|
|
673
|
+
|
|
674
|
+
---
|
|
675
|
+
|
|
609
676
|
### Issue neu zuweisen
|
|
610
677
|
|
|
611
678
|
Ändert den Bearbeiter (Handler) eines bestehenden Issues.
|