@dpesch/mantisbt-mcp-server 1.17.0 → 1.18.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 CHANGED
@@ -11,6 +11,24 @@ This project adheres to [Semantic Versioning](https://semver.org/).
11
11
 
12
12
  ---
13
13
 
14
+ ## [1.18.0] – 2026-11-09
15
+
16
+ ### Deprecated
17
+
18
+ - The default of `response` in `update_issue` and `create_issue` changes from `"full"` to `"short"` in 2.0.0. Callers that rely on the whole issue in the response should pass `response: "full"` explicitly.
19
+
20
+ ### Added
21
+
22
+ - `update_issue` and `create_issue`: optional `response` parameter. `"short"` returns only what confirms the change instead of the whole issue with description, notes and history (#8598) — for `update_issue` `id`, `summary`, `status`, `resolution`, `updated_at`, `view_url` and the changed fields (custom fields and edited notes narrowed to the changed entries, an appended note to `id` and `view_url`); for `create_issue` `id`, `summary`, `project`, `category`, `status`, `handler`, `created_at` and `view_url`. On a fresh demo issue that is 529 instead of 4,046 characters; on an issue with many notes the gap is far larger. The default stays `"full"`.
23
+ - `get_version`: `mcp_server` now also contains `commit` (short hash of the build, `-dirty` with uncommitted changes) and `built_at` (build time), so an outdated build shows even when its version number is right (#8547).
24
+
25
+ ### Fixed
26
+
27
+ - The server reported the version from `package.json` at runtime, not the one its code was built from — in the MCP handshake, `get_version`, `get_mcp_version`, the startup log and the HTTP `/health` endpoint. Between a version bump and `npm run build`, a local install therefore claimed the new version while running the old code (#8547). The build now writes `dist/build-info.json`, and the server reads its version from there. Only the release build in CI reports a plain version; every other build — a local `npm run build`, `npm run dev`, code run from `src/` — is marked `-dev`, e.g. `1.18.0-dev`. `npm publish` refuses any build that is not the release build of the package version (`prepublishOnly`). The npm package was not affected: it is always built right before publishing.
28
+ - `list_issues`: the client-side filters `status`, `assigned_to`, `reporter_id`, `updated_after`/`updated_before` and `created_after`/`created_before` returned an empty list when `select` did not contain the field they read (`status`, `handler`, `reporter`, `updated_at`, `created_at`). The `select` projection removed the field before the filter ran, so every issue failed it — silently, without an error (#8597). The fields a filter needs are now fetched as well and removed again after filtering, so the response still holds exactly the selected fields. `group_by` was not affected.
29
+
30
+ ---
31
+
14
32
  ## [1.17.0] – 2026-11-03
15
33
 
16
34
  ### Added
package/README.de.md CHANGED
@@ -156,8 +156,8 @@ Veraltete Tools gehören immer zur gleichen Gruppe wie ihr Ersatz – eine Grupp
156
156
  | `get_issue` | Ein Issue per `id` oder mehrere per `ids` (1–50) in einem Aufruf abrufen – genau eines von beiden angeben; mit `ids` lautet die Antwort `{ issues, requested, found, failed }`, nicht zugängliche IDs liefern `null` an ihrer Position; optionales `select` zur Feldauswahl |
157
157
  | `get_issues` | **Veraltet:** stattdessen `get_issue` mit `ids` verwenden – wird in 2.0.0 entfernt |
158
158
  | `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; `group_by` (`"status"`, `"handler"`, `"priority"`, `"category"`) liefert statt einer Issue-Liste Zählwerte pro Gruppe – alle Filter greifen weiterhin, `page`, `page_size` und `select` werden ignoriert |
159
- | `create_issue` | Neues Issue anlegen; `severity` und `priority` müssen kanonische englische Namen sein (z.B. `minor`, `major`, `normal`, `high`) – `get_issue_schema` 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 |
160
- | `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; `fields` darf entfallen, wenn nur `add_monitors`, `remove_monitors` oder `note` angegeben werden; mit `add_monitors` / `remove_monitors` (Arrays von Login-Namen) können Beobachter hinzugefügt oder entfernt werden – die Antwort enthält einen `monitors`-Schlüssel mit der aktuellen und der geänderten Beobachterliste; mit `fields.notes` lassen sich bestehende Notizen per ID bearbeiten (Notiz-ID aus `get_issue`) |
159
+ | `create_issue` | Neues Issue anlegen; `severity` und `priority` müssen kanonische englische Namen sein (z.B. `minor`, `major`, `normal`, `high`) – `get_issue_schema` 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; `response: "short"` liefert statt des ganzen Issues nur dessen Kernfelder (Standard `"full"`, ab 2.0.0 `"short"`) |
160
+ | `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; `fields` darf entfallen, wenn nur `add_monitors`, `remove_monitors` oder `note` angegeben werden; mit `add_monitors` / `remove_monitors` (Arrays von Login-Namen) können Beobachter hinzugefügt oder entfernt werden – die Antwort enthält einen `monitors`-Schlüssel mit der aktuellen und der geänderten Beobachterliste; mit `fields.notes` lassen sich bestehende Notizen per ID bearbeiten (Notiz-ID aus `get_issue`); `response: "short"` liefert statt des ganzen Issues mit Notizen und Historie nur die Kernfelder und die geänderten Felder (Standard `"full"`, ab 2.0.0 `"short"`) |
161
161
  | `delete_issue` | Issue löschen |
162
162
 
163
163
  ### Notizen
@@ -251,7 +251,7 @@ npm install sqlite-vec better-sqlite3
251
251
  | Tool | Beschreibung |
252
252
  |---|---|
253
253
  | `get_metadata` | Gecachte Metadaten abrufen: `detail: "summary"` (Standard) liefert Projekt-/Tag-Zählwerte und Benutzer-/Versions-/Kategorie-Zählwerte je Projekt; `detail: "full"` liefert den vollständigen rohen Cache; `refresh: true` lädt alle Daten zuerst von MantisBT nach; mit aktivierter semantischer Suche enthält die Antwort zusätzlich einen `search_index`-Block |
254
- | `get_version` | Version dieses MCP-Servers und der verbundenen MantisBT-Installation in einer Antwort; `check_latest` (Standard `true`) vergleicht MantisBT mit dem neuesten GitHub-Release; `mantis` enthält `error`, wenn MantisBT nicht erreichbar ist |
254
+ | `get_version` | Version dieses MCP-Servers und der verbundenen MantisBT-Installation in einer Antwort; der Server-Teil nennt zusätzlich Commit und Build-Zeitpunkt (Anhang `-dev`, wenn es kein veröffentlichter Release ist); `check_latest` (Standard `true`) vergleicht MantisBT mit dem neuesten GitHub-Release; `mantis` enthält `error`, wenn MantisBT nicht erreichbar ist |
255
255
  | `get_issue_schema` | Gültige `select`-Felder (`fields`, `fields_source`) und Enum-Werte (`enums`: severity, status, priority, resolution, reproducibility) für diesen MantisBT-Server in einer Antwort; optionale `project_id` schränkt die Feldermittlung ein – vor `create_issue` / `update_issue` oder beim Aufbau eines `select`-Strings verwenden |
256
256
  | `get_issue_fields` | **Veraltet:** stattdessen `get_issue_schema` verwenden – wird in 2.0.0 entfernt |
257
257
  | `get_issue_enums` | **Veraltet:** stattdessen `get_issue_schema` verwenden; die Ressource `mantis://enums` bleibt – wird in 2.0.0 entfernt |
package/README.md CHANGED
@@ -156,8 +156,8 @@ Deprecated tools always belong to the same group as their replacement — disabl
156
156
  | `get_issue` | Retrieve an issue by `id`, or several by `ids` (1–50) in one call — pass exactly one; with `ids` returns `{ issues, requested, found, failed }`, missing or inaccessible IDs give `null` at their position; optional `select` for field projection |
157
157
  | `get_issues` | **Deprecated:** use `get_issue` with `ids` instead — will be removed in 2.0.0 |
158
158
  | `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; `group_by` (`"status"`, `"handler"`, `"priority"`, `"category"`) returns counts per group instead of an issue list — all filters still apply, `page`, `page_size`, and `select` are ignored |
159
- | `create_issue` | Create a new issue; `severity` and `priority` must be canonical English names (e.g. `minor`, `major`, `normal`, `high`) — call `get_issue_schema` 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 |
160
- | `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; `fields` may be omitted when only `add_monitors`, `remove_monitors`, or `note` are given; use `add_monitors` / `remove_monitors` (arrays of login names) to add or remove watchers — the response includes a `monitors` key with the current and changed monitor lists; use `fields.notes` to edit existing notes by ID (note ID from `get_issue`) |
159
+ | `create_issue` | Create a new issue; `severity` and `priority` must be canonical English names (e.g. `minor`, `major`, `normal`, `high`) — call `get_issue_schema` 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; `response: "short"` returns only the core fields of the new issue instead of the whole issue (default `"full"`, changes to `"short"` in 2.0.0) |
160
+ | `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; `fields` may be omitted when only `add_monitors`, `remove_monitors`, or `note` are given; use `add_monitors` / `remove_monitors` (arrays of login names) to add or remove watchers — the response includes a `monitors` key with the current and changed monitor lists; use `fields.notes` to edit existing notes by ID (note ID from `get_issue`); `response: "short"` returns only the core fields and the changed ones instead of the whole issue with notes and history (default `"full"`, changes to `"short"` in 2.0.0) |
161
161
  | `delete_issue` | Delete an issue |
162
162
 
163
163
  ### Notes
@@ -251,7 +251,7 @@ npm install sqlite-vec better-sqlite3
251
251
  | Tool | Description |
252
252
  |---|---|
253
253
  | `get_metadata` | Retrieve cached metadata: `detail: "summary"` (default) returns project/tag counts and per-project user/version/category counts; `detail: "full"` returns the complete raw cache; `refresh: true` re-fetches from MantisBT first; with semantic search enabled the response also contains a `search_index` block |
254
- | `get_version` | Version of this MCP server and of the connected MantisBT installation in one response; `check_latest` (default `true`) compares MantisBT with the latest GitHub release; `mantis` holds `error` when MantisBT cannot be reached |
254
+ | `get_version` | Version of this MCP server and of the connected MantisBT installation in one response; the server part also names the commit and build time (`-dev` suffix unless it is a published release); `check_latest` (default `true`) compares MantisBT with the latest GitHub release; `mantis` holds `error` when MantisBT cannot be reached |
255
255
  | `get_issue_schema` | Valid `select` fields (`fields`, `fields_source`) and enum values (`enums`: severity, status, priority, resolution, reproducibility) for this MantisBT server in one response; optional `project_id` scopes the field discovery — use before `create_issue` / `update_issue` or when building a `select` string |
256
256
  | `get_issue_fields` | **Deprecated:** use `get_issue_schema` instead — will be removed in 2.0.0 |
257
257
  | `get_issue_enums` | **Deprecated:** use `get_issue_schema` instead; the resource `mantis://enums` stays — will be removed in 2.0.0 |
@@ -0,0 +1,17 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ /**
4
+ * Reads build-info.json, which scripts/write-build-info.mjs writes next to the
5
+ * compiled code. The version thus belongs to the code that runs, even when
6
+ * package.json was bumped after the build (#8547). Code run straight from
7
+ * src/ (tests, tsx) has no build: it reports package.json's version as -dev.
8
+ */
9
+ export function loadBuildInfo(codeDir) {
10
+ try {
11
+ return JSON.parse(readFileSync(join(codeDir, 'build-info.json'), 'utf-8'));
12
+ }
13
+ catch {
14
+ const pkg = JSON.parse(readFileSync(join(codeDir, '..', 'package.json'), 'utf-8'));
15
+ return { version: `${pkg.version}-dev` };
16
+ }
17
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "version": "1.18.0",
3
+ "commit": "65fcaba",
4
+ "built_at": "2026-11-09T13:22:17.570Z"
5
+ }
@@ -1,4 +1,7 @@
1
- /** Major version in which deprecated tools are removed (see versioning rules in CLAUDE.md). */
1
+ /**
2
+ * Next major version: deprecated tools are removed and announced default
3
+ * changes take effect (see versioning rules in CLAUDE.md).
4
+ */
2
5
  export const REMOVAL_VERSION = '2.0.0';
3
6
  /**
4
7
  * First line of the description of a deprecated tool. Keeping the wording in
package/dist/index.js CHANGED
@@ -3,9 +3,9 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
5
5
  import { createServer } from 'node:http';
6
- import { readFileSync } from 'node:fs';
7
6
  import { fileURLToPath } from 'node:url';
8
- import { join, dirname } from 'node:path';
7
+ import { dirname } from 'node:path';
8
+ import { loadBuildInfo } from './build-info.js';
9
9
  import { getConfig, getStartupConfig, assertHttpAuthConfigured } from './config.js';
10
10
  import { MantisClient } from './client.js';
11
11
  import { VersionHintService, setGlobalVersionHint } from './version-hint.js';
@@ -15,12 +15,10 @@ import { filterToolRegistration } from './toolsets.js';
15
15
  import { registerPrompts } from './prompts/index.js';
16
16
  import { registerResources } from './resources/index.js';
17
17
  // ---------------------------------------------------------------------------
18
- // Read version from package.json
18
+ // Version of the running build (dist/build-info.json, see build-info.ts)
19
19
  // ---------------------------------------------------------------------------
20
- const __filename = fileURLToPath(import.meta.url);
21
- const __dirname = dirname(__filename);
22
- const packageJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
23
- const version = packageJson.version;
20
+ const build = loadBuildInfo(dirname(fileURLToPath(import.meta.url)));
21
+ const version = build.version;
24
22
  // ---------------------------------------------------------------------------
25
23
  // Bootstrap
26
24
  // ---------------------------------------------------------------------------
@@ -57,7 +55,7 @@ async function createMcpServer(transport) {
57
55
  client,
58
56
  cache,
59
57
  versionHint,
60
- version,
58
+ build,
61
59
  uploadDir: startupConfig.uploadDir,
62
60
  transport,
63
61
  searchIndexStatus,
@@ -30,5 +30,5 @@ export function registerAllTools(server, deps) {
30
30
  registerConfigTools(server, client, cache);
31
31
  registerMetadataTools(server, client, cache, deps.searchIndexStatus);
32
32
  registerTagTools(server, client);
33
- registerVersionTools(server, client, deps.versionHint, deps.version);
33
+ registerVersionTools(server, client, deps.versionHint, deps.build);
34
34
  }
@@ -9,7 +9,7 @@ import { fetchIssueEnumsWithCache } from './config.js';
9
9
  import { postNote, noteEditsSchema, prepareNoteEdits, verifyNoteEdits } from './notes.js';
10
10
  import { applyMonitorChanges } from './monitors.js';
11
11
  import { firstIssue } from '../issue-envelope.js';
12
- import { deprecationNote } from '../deprecation.js';
12
+ import { deprecationNote, REMOVAL_VERSION } from '../deprecation.js';
13
13
  function errorText(msg) {
14
14
  const vh = getVersionHint();
15
15
  vh?.triggerLatestVersionFetch();
@@ -74,6 +74,37 @@ function makeIssueFilter({ status, assigned_to, reporter_id, dateFilter }) {
74
74
  olderThanUpdatedAfter: (issue) => updatedAfterMs !== undefined && issue.updated_at !== undefined && new Date(issue.updated_at).getTime() <= updatedAfterMs,
75
75
  };
76
76
  }
77
+ // response: "short" for create_issue / update_issue (#8598) — the fields that
78
+ // confirm what happened, instead of the whole issue with notes and history.
79
+ const responseSchema = z.enum(['short', 'full']).default('full').describe(`"full" (default): the whole issue as MantisBT returns it, including description, notes and history. ` +
80
+ `"short": only the fields that confirm the change — far fewer tokens. The default changes to "short" in ${REMOVAL_VERSION}.`);
81
+ const SHORT_CREATE_FIELDS = ['id', 'summary', 'project', 'category', 'status', 'handler', 'created_at', 'view_url'];
82
+ const SHORT_UPDATE_FIELDS = ['id', 'summary', 'status', 'resolution', 'updated_at', 'view_url'];
83
+ /** Returns only the given top-level fields of an object (those it has). */
84
+ function pickFields(obj, fields) {
85
+ return Object.fromEntries(Object.entries(obj).filter(([key]) => fields.includes(key)));
86
+ }
87
+ /**
88
+ * Short update_issue result: the core fields plus every changed field. Custom
89
+ * fields and notes are narrowed to the entries that were changed.
90
+ */
91
+ function shortUpdatedIssue(issue, changes) {
92
+ const short = pickFields(issue, [...SHORT_UPDATE_FIELDS, ...Object.keys(changes)]);
93
+ const { custom_fields: changedCustomFields, notes: editedNotes } = changes;
94
+ if (short.custom_fields && changedCustomFields) {
95
+ short.custom_fields = short.custom_fields.filter(({ field }) => changedCustomFields.some(c => c.field.id === field.id || c.field.name === field.name));
96
+ }
97
+ if (short.notes && editedNotes) {
98
+ short.notes = short.notes.filter(n => editedNotes.some(e => e.id === n.id));
99
+ }
100
+ return short;
101
+ }
102
+ /** Returns the issue without the given top-level fields. */
103
+ function omitFields(issue, fields) {
104
+ if (fields.length === 0)
105
+ return issue;
106
+ return Object.fromEntries(Object.entries(issue).filter(([key]) => !fields.includes(key)));
107
+ }
77
108
  // Worker-pool: runs `fn` over all `items` with at most `concurrency` in-flight at once.
78
109
  // nextIndex is only incremented inside microtasks, so the ++ is safe without a lock.
79
110
  async function runWithConcurrency(items, concurrency, fn) {
@@ -115,14 +146,21 @@ export function registerIssueTools(server, client, cache) {
115
146
  * the current page once MantisBT's newest-first order shows the rest is
116
147
  * older than updated_after. Returns truncated when maxPages ended the scan
117
148
  * before either happened.
149
+ *
150
+ * select is the caller's projection. The filter reads its fields from each
151
+ * issue, so a select that omits them would fail every issue (#8597): they
152
+ * are fetched as well and removed again before onMatch sees the issue.
118
153
  */
119
- async function scanMatchingIssues(params, filter, pageSize, maxPages, onMatch) {
154
+ async function scanMatchingIssues(params, select, filter, pageSize, maxPages, onMatch) {
155
+ const selected = select?.split(',').map(f => f.trim()).filter(Boolean);
156
+ const extraFields = selected ? filter.fields.filter(f => !selected.includes(f)) : [];
157
+ const fetchSelect = selected && [...selected, ...extraFields].join(',');
120
158
  for (let page = 1; page <= maxPages; page++) {
121
- const issues = (await client.get('issues', { ...params, page, page_size: pageSize })).issues ?? [];
159
+ const issues = (await client.get('issues', { ...params, select: fetchSelect, page, page_size: pageSize })).issues ?? [];
122
160
  let stopAfterBatch = false;
123
161
  for (const issue of issues) {
124
162
  if (filter.matches(issue)) {
125
- if (onMatch(issue) === false)
163
+ if (onMatch(omitFields(issue, extraFields)) === false)
126
164
  return { truncated: false };
127
165
  }
128
166
  else if (filter.olderThanUpdatedAfter(issue)) {
@@ -139,10 +177,9 @@ export function registerIssueTools(server, client, cache) {
139
177
  * the fields grouping and filters need.
140
178
  */
141
179
  async function groupIssues(groupBy, params, filter) {
142
- const select = [...new Set(['id', groupBy, ...filter.fields])].join(',');
143
180
  const groups = new Map();
144
181
  let total = 0;
145
- const { truncated } = await scanMatchingIssues({ ...params, select }, filter, GROUP_BY_PAGE_SIZE, GROUP_BY_MAX_PAGES, issue => {
182
+ const { truncated } = await scanMatchingIssues(params, `id,${groupBy}`, filter, GROUP_BY_PAGE_SIZE, GROUP_BY_MAX_PAGES, issue => {
146
183
  const ref = issue[groupBy];
147
184
  const key = ref?.id ?? null;
148
185
  let group = groups.get(key);
@@ -300,7 +337,7 @@ Use "select" to fetch only some fields, e.g. select: "notes" for just the notes
300
337
  const MAX_API_PAGES = 10; // hard cap: scan at most 500 issues
301
338
  const neededTotal = page * page_size; // need this many matches to serve page N
302
339
  const matching = [];
303
- const { truncated } = await scanMatchingIssues(baseParams, filter, API_PAGE_SIZE, MAX_API_PAGES, issue => {
340
+ const { truncated } = await scanMatchingIssues(baseParams, select, filter, API_PAGE_SIZE, MAX_API_PAGES, issue => {
304
341
  matching.push(issue);
305
342
  return matching.length < neededTotal;
306
343
  });
@@ -324,7 +361,7 @@ Use "select" to fetch only some fields, e.g. select: "notes" for just the notes
324
361
  // ---------------------------------------------------------------------------
325
362
  server.registerTool('create_issue', {
326
363
  title: 'Create Issue',
327
- description: `Create a new MantisBT issue. Returns the full created issue object including the assigned id, summary, status, priority, severity, category, reporter, created_at, and view_url.
364
+ description: `Create a new MantisBT issue. Returns the full created issue object including the assigned id, summary, status, priority, severity, category, reporter, created_at, and view_url. With response: "short" only id, summary, project, category, status, handler, created_at and view_url (default "full"; it changes to "short" in ${REMOVAL_VERSION}).
328
365
 
329
366
  Required fields: summary, description, project_id, category. All other fields are optional with sensible defaults (priority: "normal", severity: "minor").
330
367
 
@@ -353,13 +390,14 @@ For the handler, prefer the username field (resolved server-side) over handler_i
353
390
  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_schema to see localized labels.'),
354
391
  view_state: z.enum(['public', 'private']).optional().describe('Visibility of the issue: "public" (visible to all, default) or "private" (restricted to higher-access users).'),
355
392
  custom_fields: z.array(customFieldEntry).optional().describe('Custom field values: [{field: {id|name}, value: "<string>"}]. Use get_issue_schema or get_metadata to discover available custom fields per project.'),
393
+ response: responseSchema,
356
394
  }),
357
395
  annotations: {
358
396
  readOnlyHint: false,
359
397
  destructiveHint: false,
360
398
  idempotentHint: false,
361
399
  },
362
- }, 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 }) => {
400
+ }, 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, response }) => {
363
401
  // Resolve handler username to handler_id when only a name is given
364
402
  let resolvedHandlerId = handler_id;
365
403
  if (resolvedHandlerId === undefined && handler !== undefined) {
@@ -425,19 +463,22 @@ For the handler, prefer the username field (resolved server-side) over handler_i
425
463
  const partial = firstIssue(raw) ?? raw;
426
464
  let issue = partial;
427
465
  if (!('summary' in partial)) {
428
- // Older MantisBT returned only { id: N } — fetch the full issue.
466
+ // Older MantisBT returned only { id: N } — fetch the issue (only the
467
+ // short fields when those are all that is returned).
429
468
  // Suppress GET errors: the issue was already created.
430
469
  try {
431
- const fetched = await client.get(`issues/${partial.id}`);
470
+ const select = response === 'short' ? SHORT_CREATE_FIELDS.filter(f => f !== 'view_url').join(',') : undefined;
471
+ const fetched = await client.get(`issues/${partial.id}`, { select });
432
472
  issue = firstIssue(fetched) ?? partial;
433
473
  }
434
474
  catch {
435
475
  // unable to fetch details — return minimal object
436
476
  }
437
477
  }
438
- const baseUrl = await client.getBaseUrl();
478
+ const enriched = enrichIssue(issue, await client.getBaseUrl());
479
+ const result = response === 'short' ? pickFields(enriched, SHORT_CREATE_FIELDS) : enriched;
439
480
  return {
440
- content: [{ type: 'text', text: JSON.stringify(enrichIssue(issue, baseUrl), null, 2) }],
481
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
441
482
  };
442
483
  }
443
484
  catch (error) {
@@ -495,6 +536,8 @@ Use the optional "note" parameter to append a note in the same call (e.g. the re
495
536
 
496
537
  Use "add_monitors" / "remove_monitors" (lists of MantisBT login names) to change who watches the issue — "fields" may be omitted then. The response contains "monitors" with added/removed/not_monitoring and the current monitor list. Adding or removing other users requires the monitor_add_others_bug_threshold / monitor_delete_others_bug_threshold access level.
497
538
 
539
+ The response is the updated issue. With response: "short" it holds only id, summary, status, resolution, updated_at, view_url and the fields that were changed (custom_fields and notes narrowed to the changed entries; an appended note as { id, view_url }) — use it for routine changes. Default is "full"; it changes to "short" in ${REMOVAL_VERSION}.
540
+
498
541
  Steps run in order: fields, monitors, note. Once one step succeeded, a later failure is reported inline ("notes_error" when a note edit was not applied, "monitors_error", "note_error") instead of failing the call — do not retry the steps that went through. Only if every requested step failed is the call an error.`,
499
542
  inputSchema: z.object({
500
543
  id: z.coerce.number().int().positive().describe('Numeric issue ID to update'),
@@ -503,6 +546,7 @@ Steps run in order: fields, monitors, note. Once one step succeeded, a later fai
503
546
  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.'),
504
547
  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.'),
505
548
  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.'),
549
+ response: responseSchema,
506
550
  fields: z.preprocess((v) => {
507
551
  if (typeof v !== 'string')
508
552
  return v;
@@ -537,7 +581,7 @@ Steps run in order: fields, monitors, note. Once one step succeeded, a later fai
537
581
  destructiveHint: false,
538
582
  idempotentHint: false,
539
583
  },
540
- }, async ({ id, fields, dry_run, note, note_view_state, add_monitors, remove_monitors }) => {
584
+ }, async ({ id, fields, dry_run, note, note_view_state, add_monitors, remove_monitors, response: responseMode }) => {
541
585
  const fieldChanges = fields ?? {};
542
586
  const hasFields = Object.keys(fieldChanges).length > 0;
543
587
  const hasMonitorChanges = add_monitors !== undefined || remove_monitors !== undefined;
@@ -574,9 +618,10 @@ Steps run in order: fields, monitors, note. Once one step succeeded, a later fai
574
618
  }],
575
619
  };
576
620
  }
621
+ const short = responseMode === 'short';
577
622
  const patched = hasFields ? await patchIssueFields(id, patchFields) : undefined;
578
623
  const response = patched
579
- ? { ...patched }
624
+ ? (short ? shortUpdatedIssue(patched, fieldChanges) : { ...patched })
580
625
  : { id, view_url: buildIssueViewUrl(await client.getBaseUrl(), id) };
581
626
  // MantisBT skips note edits it does not apply without an error, so the
582
627
  // response is checked.
@@ -614,7 +659,8 @@ Steps run in order: fields, monitors, note. Once one step succeeded, a later fai
614
659
  }
615
660
  if (note !== undefined) {
616
661
  try {
617
- response['note'] = await postNote(client, id, note, note_view_state);
662
+ const posted = await postNote(client, id, note, note_view_state);
663
+ response['note'] = short ? pickFields(posted, ['id', 'view_url']) : posted;
618
664
  succeeded = true;
619
665
  }
620
666
  catch (noteError) {
@@ -2,7 +2,7 @@ import { z } from 'zod';
2
2
  import { coerceBool } from '../coerce.js';
3
3
  import { parseVersion, compareVersions } from '../version-hint.js';
4
4
  import { deprecationNote } from '../deprecation.js';
5
- export function registerVersionTools(server, client, versionHint, mcpVersion) {
5
+ export function registerVersionTools(server, client, versionHint, build) {
6
6
  /**
7
7
  * Installed MantisBT version (X-Mantis-Version header), optionally compared
8
8
  * with the latest MantisBT release on GitHub.
@@ -40,7 +40,9 @@ export function registerVersionTools(server, client, versionHint, mcpVersion) {
40
40
  server.registerTool('get_version', {
41
41
  title: 'Get Versions',
42
42
  description: `Return the version of this mantisbt-mcp-server and of the connected MantisBT installation in one response:
43
- { mcp_server: { version }, mantis: { installed_version, latest_version?, status?, github_note? } }
43
+ { mcp_server: { version, commit?, built_at? }, mantis: { installed_version, latest_version?, status?, github_note? } }
44
+
45
+ mcp_server describes the running build: version ends in "-dev" unless it is a published release; commit is the short hash it was built from ("-dirty" with uncommitted changes), built_at the ISO-8601 build time.
44
46
 
45
47
  The MantisBT version is read from the X-Mantis-Version response header. With check_latest (default: true) it is compared against the latest MantisBT release on GitHub (outbound HTTPS request, up to 5 seconds); status is "up-to-date", "update-available", "newer-than-release" or "unknown". If MantisBT cannot be reached, mantis contains "error" instead and mcp_server is still returned.`,
46
48
  inputSchema: z.object({ check_latest: checkLatestSchema }),
@@ -54,7 +56,7 @@ The MantisBT version is read from the X-Mantis-Version response header. With che
54
56
  error: error instanceof Error ? error.message : String(error),
55
57
  }));
56
58
  return {
57
- content: [{ type: 'text', text: JSON.stringify({ mcp_server: { version: mcpVersion }, mantis }, null, 2) }],
59
+ content: [{ type: 'text', text: JSON.stringify({ mcp_server: build, mantis }, null, 2) }],
58
60
  };
59
61
  });
60
62
  // ---------------------------------------------------------------------------
@@ -70,7 +72,7 @@ The MantisBT version is read from the X-Mantis-Version response header. With che
70
72
  idempotentHint: true,
71
73
  },
72
74
  }, async () => ({
73
- content: [{ type: 'text', text: JSON.stringify({ version: mcpVersion }, null, 2) }],
75
+ content: [{ type: 'text', text: JSON.stringify({ version: build.version }, null, 2) }],
74
76
  }));
75
77
  // ---------------------------------------------------------------------------
76
78
  // get_mantis_version
@@ -779,6 +779,47 @@ Löst ein Issue auf und schließt es. **Immer beide Felder** `status` und `resol
779
779
 
780
780
  ---
781
781
 
782
+ ### Kurze Antwort statt des ganzen Issues
783
+
784
+ Standardmäßig geben `update_issue` und `create_issue` das ganze Issue zurück, mit Beschreibung, Notizen und Historie – bei einem lebhaften Issue mehrere tausend Token. Mit `response: "short"` kommt nur zurück, was die Änderung bestätigt.
785
+
786
+ **Tool:** `update_issue` (bei `create_issue` genauso)
787
+
788
+ **Parameter:**
789
+ - `response` – _(optional)_ `"full"` (Standard) oder `"short"`. In 2.0.0 wird `"short"` zum Standard.
790
+
791
+ **Request:**
792
+
793
+ ```json
794
+ {
795
+ "id": 1042,
796
+ "fields": {
797
+ "status": { "name": "resolved" },
798
+ "resolution": { "id": 20 }
799
+ },
800
+ "note": "Behoben in Commit abc123.",
801
+ "response": "short"
802
+ }
803
+ ```
804
+
805
+ **Response:**
806
+
807
+ ```json
808
+ {
809
+ "id": 1042,
810
+ "summary": "Login schlägt auf iOS fehl",
811
+ "status": { "id": 80, "name": "resolved", "label": "erledigt" },
812
+ "resolution": { "id": 20, "name": "fixed", "label": "erledigt" },
813
+ "updated_at": "2026-11-09T13:21:00+01:00",
814
+ "view_url": "https://mantis.example.com/view.php?id=1042",
815
+ "note": { "id": 4957, "view_url": "https://mantis.example.com/view.php?id=1042#bugnote4957" }
816
+ }
817
+ ```
818
+
819
+ `update_issue` liefert `id`, `summary`, `status`, `resolution`, `updated_at`, `view_url` und jedes geänderte Feld; `custom_fields` und `notes` (Änderungen über `fields.notes`) sind auf die geänderten Einträge beschränkt, eine angehängte Notiz auf `id` und `view_url`. `monitors` und die `*_error`-Schlüssel erscheinen wie bei `"full"`. `create_issue` liefert `id`, `summary`, `project`, `category`, `status`, `handler`, `created_at` und `view_url`.
820
+
821
+ ---
822
+
782
823
  ### Benutzerdefinierte Felder setzen
783
824
 
784
825
  Schreibt Custom-Field-Werte beim Anlegen oder Aktualisieren. Dasselbe `custom_fields`-Format funktioniert bei `create_issue` und `update_issue` (dort innerhalb von `fields`).
@@ -2344,7 +2385,7 @@ Gibt die Version dieses mantisbt-mcp-servers und der verbundenen MantisBT-Instal
2344
2385
 
2345
2386
  ```json
2346
2387
  {
2347
- "mcp_server": { "version": "1.15.0" },
2388
+ "mcp_server": { "version": "1.18.0", "commit": "9ebef4c", "built_at": "2026-11-09T12:00:00.000Z" },
2348
2389
  "mantis": {
2349
2390
  "installed_version": "2.27.0",
2350
2391
  "latest_version": "2.27.1",
@@ -2355,6 +2396,8 @@ Gibt die Version dieses mantisbt-mcp-servers und der verbundenen MantisBT-Instal
2355
2396
 
2356
2397
  Mögliche Werte für `status`: `up-to-date`, `update-available`, `newer-than-release`, `unknown`. Mit `check_latest: false` den GitHub-Vergleich überspringen.
2357
2398
 
2399
+ `mcp_server` beschreibt den laufenden Build, nicht die aktuelle `package.json`: `version` ist die Version, aus der gebaut wurde, `commit` der kurze Commit-Hash (mit `-dirty`, wenn es nicht committete Änderungen gab), `built_at` der Build-Zeitpunkt. Nur ein veröffentlichter Release meldet eine reine Versionsnummer; ein lokaler Build, `npm run dev` oder Code direkt aus `src/` meldet z. B. `1.18.0-dev`. Ohne Git fehlt `commit`; Code direkt aus `src/` hat weder `commit` noch `built_at`.
2400
+
2358
2401
  > **Veraltet:** `get_mcp_version` und `get_mantis_version` werden in 2.0.0 entfernt – stattdessen `get_version` wie oben gezeigt verwenden.
2359
2402
 
2360
2403
  ---
package/docs/cookbook.md CHANGED
@@ -779,6 +779,47 @@ Changes the status and appends a note explaining the change — in one `update_i
779
779
 
780
780
  ---
781
781
 
782
+ ### Get a short response instead of the whole issue
783
+
784
+ By default `update_issue` and `create_issue` return the whole issue, including description, notes and history — several thousand tokens on a busy issue. With `response: "short"` you get only what confirms the change.
785
+
786
+ **Tool:** `update_issue` (works the same way for `create_issue`)
787
+
788
+ **Parameters:**
789
+ - `response` — _(optional)_ `"full"` (default) or `"short"`. The default changes to `"short"` in 2.0.0.
790
+
791
+ **Request:**
792
+
793
+ ```json
794
+ {
795
+ "id": 1042,
796
+ "fields": {
797
+ "status": { "name": "resolved" },
798
+ "resolution": { "id": 20 }
799
+ },
800
+ "note": "Fixed in commit abc123.",
801
+ "response": "short"
802
+ }
803
+ ```
804
+
805
+ **Response:**
806
+
807
+ ```json
808
+ {
809
+ "id": 1042,
810
+ "summary": "Login fails on iOS",
811
+ "status": { "id": 80, "name": "resolved", "label": "resolved" },
812
+ "resolution": { "id": 20, "name": "fixed", "label": "fixed" },
813
+ "updated_at": "2026-11-09T13:21:00+01:00",
814
+ "view_url": "https://mantis.example.com/view.php?id=1042",
815
+ "note": { "id": 4957, "view_url": "https://mantis.example.com/view.php?id=1042#bugnote4957" }
816
+ }
817
+ ```
818
+
819
+ `update_issue` returns `id`, `summary`, `status`, `resolution`, `updated_at`, `view_url` and every field you changed; `custom_fields` and `notes` (edits via `fields.notes`) are narrowed to the entries you changed, an appended note to `id` and `view_url`. `monitors` and the `*_error` keys appear as with `"full"`. `create_issue` returns `id`, `summary`, `project`, `category`, `status`, `handler`, `created_at` and `view_url`.
820
+
821
+ ---
822
+
782
823
  ### Set custom fields
783
824
 
784
825
  Writes custom field values on create or update. The same `custom_fields` format works for both `create_issue` and `update_issue` (inside `fields`).
@@ -2344,7 +2385,7 @@ Returns the version of this mantisbt-mcp-server and of the connected MantisBT in
2344
2385
 
2345
2386
  ```json
2346
2387
  {
2347
- "mcp_server": { "version": "1.15.0" },
2388
+ "mcp_server": { "version": "1.18.0", "commit": "9ebef4c", "built_at": "2026-11-09T12:00:00.000Z" },
2348
2389
  "mantis": {
2349
2390
  "installed_version": "2.27.0",
2350
2391
  "latest_version": "2.27.1",
@@ -2355,6 +2396,8 @@ Returns the version of this mantisbt-mcp-server and of the connected MantisBT in
2355
2396
 
2356
2397
  Possible values for `status`: `up-to-date`, `update-available`, `newer-than-release`, `unknown`. Set `check_latest: false` to skip the GitHub comparison.
2357
2398
 
2399
+ `mcp_server` describes the running build, not the current `package.json`: `version` is the version it was built from, `commit` the short commit hash (with `-dirty` if there were uncommitted changes), `built_at` the build time. Only a published release reports a plain version; a local build, `npm run dev` or code run straight from `src/` reports e.g. `1.18.0-dev`. Without git, `commit` is left out; code run from `src/` has neither `commit` nor `built_at`.
2400
+
2358
2401
  > **Deprecated:** `get_mcp_version` and `get_mantis_version` will be removed in 2.0.0 — use `get_version` as shown above.
2359
2402
 
2360
2403
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dpesch/mantisbt-mcp-server",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "mcpName": "io.github.dpesch/mantisbt-mcp-server",
5
5
  "description": "MCP server for MantisBT REST API – read and manage bug tracker issues",
6
6
  "author": "Dominik Pesch",
@@ -32,10 +32,11 @@
32
32
  },
33
33
  "type": "module",
34
34
  "scripts": {
35
- "build": "tsc -p tsconfig.build.json",
35
+ "build": "tsc -p tsconfig.build.json && node scripts/write-build-info.mjs",
36
36
  "typecheck": "tsc --noEmit",
37
37
  "start": "node dist/index.js",
38
- "dev": "tsc -p tsconfig.build.json --watch",
38
+ "dev": "node scripts/write-build-info.mjs && tsc -p tsconfig.build.json --watch",
39
+ "prepublishOnly": "node scripts/check-release-build.mjs",
39
40
  "dev:start": "npm run typecheck && npm run build && node dist/index.js",
40
41
  "test": "vitest run",
41
42
  "test:watch": "vitest",
package/server.json CHANGED
@@ -3,12 +3,12 @@
3
3
  "name": "io.github.dpesch/mantisbt-mcp-server",
4
4
  "title": "MantisBT MCP Server",
5
5
  "description": "MantisBT MCP server – manage issues, notes, files, tags, and relationships. With semantic search.",
6
- "version": "1.17.0",
6
+ "version": "1.18.0",
7
7
  "packages": [
8
8
  {
9
9
  "registryType": "npm",
10
10
  "identifier": "@dpesch/mantisbt-mcp-server",
11
- "version": "1.17.0",
11
+ "version": "1.18.0",
12
12
  "runtimeHint": "npx",
13
13
  "transport": {
14
14
  "type": "stdio"