@nextoolsolutions/mcp-glpi-core 0.0.0-stage → 1.3.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/CHANGELOG.md ADDED
@@ -0,0 +1,95 @@
1
+ # Changelog — @nextoolsolutions/mcp-glpi-core
2
+
3
+ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions follow semver.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [1.3.2] - 2026-10-05
8
+
9
+ ### Fixed
10
+ - **`openWorldHint` is now `true` on every tool.** The tools reach the GLPI instance the user connected, an external
11
+ system controlled by that organization; the MCP spec calls that open-world, and the OpenAI plugin scan flagged
12
+ `false` on every tool. Other annotations are unchanged.
13
+
14
+ ## [1.3.1] - 2026-10-04
15
+
16
+ ### Fixed
17
+ - **Raw HTML in markdown tables for nested richtext.** `pickFields` flattens richtext only at the top
18
+ level, so the API v2 timeline (`{type, item: {content: "<p>…</p>"}}`) reached the markdown summary
19
+ as `<p>Olá <strong></strong>…`. In `format: "markdown"` (default `fields: "essential"`) the payload
20
+ now goes through `flattenRichtext` before the view and the table: richtext fields are turned into
21
+ text at any depth (up to 3 levels). The JSON result and `fields: "all"` keep the HTML as GLPI sent it.
22
+ - Table cells turn a run of line breaks into one space (a flattened paragraph left double spaces).
23
+
24
+ ### Added
25
+ - `flattenRichtext(value, maxDepth = 3)` and `MARKDOWN_RICHTEXT_FIELDS` (`RICHTEXT_FIELDS` plus the
26
+ validation comments `comment_submission`, `comment_validation`, `submission_comment`,
27
+ `approval_comment`, which GLPI 11 stores as HTML).
28
+
29
+ ## [1.3.0] - 2026-10-04
30
+
31
+ ### Added
32
+ - `markdownViews` option of `installPayloadFormatting` and `columnView` / `MarkdownColumn` /
33
+ `MarkdownView`: per-tool column projection for listings in `format: "markdown"` (only with the
34
+ default `fields: "essential"`; the JSON result is never projected). The size budget measures the
35
+ projected table, so more rows fit.
36
+
37
+ ### Changed
38
+ - `ESSENTIAL_FIELDS.Ticket` keeps `requesters` and `assigned` (the `{id, name}` lists
39
+ `mcp-glpi` 3.5.0 adds to ticket listings).
40
+
41
+ ## [1.2.0] - 2026-10-04
42
+
43
+ ### Added
44
+ - Size budget for every read tool (`installPayloadFormatting`): texts longer than
45
+ `LIST_TEXT_MAX_CHARS` (300, `GLPI_LIST_TEXT_MAX_CHARS`) are cut in listings (except the ticket
46
+ history listings in `FULL_TEXT_LISTS`, and with `fields: "all"`); an answer longer than
47
+ `MAX_RESPONSE_CHARS` (50000, `GLPI_MAX_RESPONSE_CHARS`) is trimmed from the end of the list.
48
+ - `nextPageNote`: the pagination note names only the parameters the tool has (`range`, or
49
+ `start`/`limit`) with the values of the next page, and uses `total` when the result carries it.
50
+ - `cutLongTexts`, `FULL_TEXT_LISTS`.
51
+
52
+ ### Changed
53
+ - **`format: "markdown"` is carried in `structuredContent` too** (`{ data: "<markdown>",
54
+ format: "markdown", count?, note? }`). Clients that support structured results hand that to
55
+ the model, so a markdown text block alone was ignored. Sibling keys of `data` (e.g. `total`)
56
+ are kept in the rendering. A single item renders as `key: value` lines without cutting texts;
57
+ `{id, name}` references render as "name (id)".
58
+ - Results whose payload is the structured object itself (API v2 single items) are formatted
59
+ too; before, `fields` and `format` were silently ignored on them.
60
+ - `TOOL_ITEMTYPES` holds API v1 tools only: v2 payloads use other field names and the v1
61
+ whitelists stripped their relations (`entity`, `category`, `team`). v2 tools use the generic
62
+ blocklist, which now also drops `*_duration` and `internal_*`.
63
+ - Whitelists keep keys ending in `_name` (names resolved by the server) and numeric keys (search
64
+ columns). `glpi_search` is no longer a generic-itemtype tool.
65
+ - `MAX_PAGE_SIZE` default 200 -> 100. `paginationNote` no longer names a parameter.
66
+ - `errorResult` has no `structuredContent`: the MCP SDK client validates it against the output
67
+ schema even on errors, which replaced every GLPI error message with a schema error.
68
+ - `sanitizeId` refuses an empty ID (it built the collection URL).
69
+
70
+ ## [1.1.0] - 2026-10-04
71
+
72
+ ### Added
73
+ - `FetchImpl` type and an optional `fetchImpl` argument on `fetchFn` / `fetchWithTimeout`:
74
+ callers inject their own `fetch` (e.g. one that validates the resolved IP).
75
+ - `GlpiRedirectError` (extends `GlpiHttpError`, with `targetHost`) and `assertNotRedirect`.
76
+ - `titleFromToolName` and the `ToolAnnotationHints` type.
77
+
78
+ ### Changed
79
+ - **Redirects are never followed.** Every request is sent with `redirect: "manual"`; a 3xx (or an
80
+ opaque redirect) throws `redirect not followed: <status> -> <host>`. Only the host of the
81
+ Location is reported, never its path or query. The Node polyfill now exposes `location`.
82
+ - **Annotations.** `annotationsFor(kind, toolName?)`: `openWorldHint` is now `false`;
83
+ `idempotentHint` is `true` for reads, destructive tools and `update_*` / `set_*` writes.
84
+ `destructiveHint` is `true` for deletes and for every write that is not additive
85
+ (`update_*`, `set_*`, `change_*`, `retry_*`): the MCP spec defines `false` as "only additive
86
+ updates", so only `create_*` / `add_*` stay non-destructive.
87
+ `installWritePolicy` also sets `annotations.title` (the tool's `title`, or one derived from
88
+ its name).
89
+ - LICENSE (MIT, NexTool Solutions) and CHANGELOG ship in the package.
90
+
91
+ ## [1.0.0] - 2026-08-31
92
+
93
+ ### Added
94
+ - Shared HTTP transport with retry/timeout, typed errors, tool-result helpers, write policy,
95
+ payload formatting, pagination and create idempotency for the GLPI MCP servers.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NexTool Solutions
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,71 @@
1
- # Temporary Holding Version
1
+ # @nextoolsolutions/mcp-glpi-core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Shared infrastructure for NexTool MCP for GLPI (`@nextoolsolutions/mcp-glpi`, API v1 and v2
4
+ families). It started as the byte-identical code the old `mcp-glpi` and `mcp-glpi-v2` servers
5
+ both carried.
6
+
7
+ > Not affiliated with Teclib'. GLPI is a registered trademark of Teclib'.
8
+
9
+ | Module | What it holds |
10
+ |--------|---------------|
11
+ | `http.ts` | Injectable fetch (`FetchImpl`), polyfill, timeout, redirect refusal, retry/backoff helpers, URL helpers, ID sanitisation |
12
+ | `errors.ts` | `GlpiHttpError` base class, `GlpiRedirectError`, `GlpiPolicyError` |
13
+ | `result.ts` | `toolResult` / `jsonResult` / `errorResult` / `makeWrap` |
14
+ | `policy.ts` | Write policy: read-only mode, delete gating, tool classification by name |
15
+ | `server-policy.ts` | Installs the policy and MCP annotations (title, read-only, destructive, idempotent, closed world) by wrapping `registerTool` |
16
+ | `format.ts` | HTML stripping, per-itemtype field whitelists (API v1), markdown rendering |
17
+ | `pagination.ts` | Range/limit defaults and ceiling, size budget constants, next-page notes |
18
+ | `server-format.ts` | Installs payload formatting, pagination and the size budget on read tools; markdown goes in the text and in `structuredContent` |
19
+ | `idempotency.ts` | Create idempotency key and store |
20
+ | `server-idempotency.ts` | Installs the create guard, including in-flight collapsing |
21
+
22
+ ## Design note
23
+
24
+ The three `install*` functions wrap `server.registerTool` once, right after the server is
25
+ constructed, instead of editing 144 call sites. Every tool registered afterwards — present or
26
+ future — is covered, and misclassification fails the test suite rather than leaking a write.
27
+ They compose in any order.
28
+
29
+ ## Transport guarantees
30
+
31
+ - `fetchFn(url, init, fetchImpl?)` / `fetchWithTimeout(...)` use the injected `fetchImpl`, else
32
+ the global `fetch`, else a Node `http`/`https` polyfill.
33
+ - Requests always go out with `redirect: "manual"`. A 3xx throws `GlpiRedirectError`
34
+ (`redirect not followed: <status> -> <host>`); the Location path and query never reach the
35
+ message. Clients treat it as final (no retry).
36
+
37
+ ## Read tool output (1.2.0)
38
+
39
+ - `format: "markdown"` is returned in the text block and in `structuredContent`
40
+ (`{ data: "<markdown>", format: "markdown", count, note }`), for wrapped (`{ data }`) and
41
+ unwrapped (API v2 item) payloads alike.
42
+ - Listings: at most `GLPI_MAX_PAGE_SIZE` (100) items, texts cut at `GLPI_LIST_TEXT_MAX_CHARS`
43
+ (300) except in `FULL_TEXT_LISTS`, answer trimmed to `GLPI_MAX_RESPONSE_CHARS` (50000). The
44
+ `note` names only the tool's own pagination parameters.
45
+ - `errorResult` carries no `structuredContent`, so validating clients show the error message.
46
+ - Markdown views (1.3.0): `installPayloadFormatting({ markdownViews })` maps a tool name to a row
47
+ projection (`columnView([{ label, from }])`, `from` = a key, candidate keys or a function). In
48
+ `format: "markdown"` with `fields: "essential"` the listing table shows only those columns; the
49
+ JSON result and `fields: "all"` keep every field. The `Ticket` whitelist keeps `requesters` and
50
+ `assigned` (people resolved by the server).
51
+ - Nested richtext in markdown (1.3.1): `flattenRichtext` turns HTML into text in richtext fields at
52
+ any depth (the API v2 timeline's `item.content`, validation comments) before a markdown table or
53
+ item is drawn; the JSON result and `fields: "all"` keep the HTML.
54
+
55
+ ## Consuming it
56
+
57
+ `@nextoolsolutions/mcp-glpi` declares it as `^1.3.1`. Inside the repo its lockfile links the
58
+ sibling folder, so build here first — `tsx` does not transpile TypeScript inside
59
+ `node_modules`, the server loads the compiled `dist/`:
60
+
61
+ ```bash
62
+ cd mcp-glpi-core && npm ci && npm run build # after every change
63
+ ```
64
+
65
+ Publish this package before any `mcp-glpi` release that needs a new core version.
66
+
67
+ ## Tests
68
+
69
+ ```bash
70
+ npm test # node --test via tsx, no extra dependency
71
+ ```
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Shared error types for the GLPI MCP servers.
3
+ *
4
+ * Both mcp-glpi (REST v1) and mcp-glpi-v2 (API v2 / OAuth2) used to declare an
5
+ * identical error class. They now extend this base so callers can catch a
6
+ * single type while each server keeps its own `name` for readable messages.
7
+ */
8
+ export declare class GlpiHttpError extends Error {
9
+ readonly status: number;
10
+ readonly method: string;
11
+ readonly path: string;
12
+ constructor(message: string, status: number, method: string, path: string, name?: string);
13
+ }
14
+ /** Raised when the write policy blocks an operation before it reaches GLPI. */
15
+ export declare class GlpiPolicyError extends Error {
16
+ constructor(message: string);
17
+ }
18
+ /**
19
+ * Raised when GLPI answers a request with a 3xx. Requests go out with
20
+ * `redirect: "manual"`, so a redirect is never followed: following one would
21
+ * send the session/OAuth headers to whatever host the Location names. Only the
22
+ * target host is kept in the message — the Location path and query may carry
23
+ * tokens.
24
+ */
25
+ export declare class GlpiRedirectError extends GlpiHttpError {
26
+ readonly targetHost: string;
27
+ constructor(status: number, method: string, path: string, targetHost: string);
28
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Shared error types for the GLPI MCP servers.
3
+ *
4
+ * Both mcp-glpi (REST v1) and mcp-glpi-v2 (API v2 / OAuth2) used to declare an
5
+ * identical error class. They now extend this base so callers can catch a
6
+ * single type while each server keeps its own `name` for readable messages.
7
+ */
8
+ export class GlpiHttpError extends Error {
9
+ status;
10
+ method;
11
+ path;
12
+ constructor(message, status, method, path, name = "GlpiHttpError") {
13
+ super(message);
14
+ this.status = status;
15
+ this.method = method;
16
+ this.path = path;
17
+ this.name = name;
18
+ }
19
+ }
20
+ /** Raised when the write policy blocks an operation before it reaches GLPI. */
21
+ export class GlpiPolicyError extends Error {
22
+ constructor(message) {
23
+ super(message);
24
+ this.name = "GlpiPolicyError";
25
+ }
26
+ }
27
+ /**
28
+ * Raised when GLPI answers a request with a 3xx. Requests go out with
29
+ * `redirect: "manual"`, so a redirect is never followed: following one would
30
+ * send the session/OAuth headers to whatever host the Location names. Only the
31
+ * target host is kept in the message — the Location path and query may carry
32
+ * tokens.
33
+ */
34
+ export class GlpiRedirectError extends GlpiHttpError {
35
+ targetHost;
36
+ constructor(status, method, path, targetHost) {
37
+ super(`redirect not followed: ${status} -> ${targetHost}`, status, method, path, "GlpiRedirectError");
38
+ this.targetHost = targetHost;
39
+ }
40
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Payload formatting for the GLPI MCP servers.
3
+ *
4
+ * GLPI returns everything it has: a single ticket carries 45 fields, roughly
5
+ * twenty of which are SLA/OLA bookkeeping and delay statistics no agent ever
6
+ * reads, and richtext fields arrive as double-escaped TinyMCE HTML
7
+ * (`&#60;div class="elementToProof"&#62;Teste&#60;/div&#62;`). Returning that raw
8
+ * burns context on every call.
9
+ *
10
+ * `essential` mode keeps the fields that describe the item; `all` returns the
11
+ * untouched payload for the cases where a rare field is genuinely needed.
12
+ */
13
+ export type FieldMode = "essential" | "all";
14
+ export type OutputFormat = "json" | "markdown";
15
+ /**
16
+ * Turns a TinyMCE richtext field into plain text.
17
+ *
18
+ * Entities are decoded twice on purpose: GLPI stores the markup escaped, so a
19
+ * first pass turns `&#60;div&#62;` into real tags that the tag stripper can
20
+ * remove, and a second pass decodes the entities that were inside the text.
21
+ */
22
+ export declare function stripHtml(value: string): string;
23
+ /** Fields whose content is richtext and should be flattened to plain text. */
24
+ export declare const RICHTEXT_FIELDS: Set<string>;
25
+ /**
26
+ * Richtext fields flattened for markdown: RICHTEXT_FIELDS plus the validation
27
+ * comments (v1 comment_submission / comment_validation, v2 submission_comment /
28
+ * approval_comment), which GLPI 11 also stores as HTML.
29
+ */
30
+ export declare const MARKDOWN_RICHTEXT_FIELDS: Set<string>;
31
+ /**
32
+ * Flattens richtext fields to plain text at any depth up to `maxDepth` nested
33
+ * objects/arrays. `pickFields` only flattens the top level, so the API v2
34
+ * timeline (`{type, item: {content: "<p>…</p>"}}`) reached the markdown table
35
+ * as raw HTML. Used for markdown only: the JSON result keeps nested fields as
36
+ * GLPI sent them. Returns a copy; the input is not modified.
37
+ */
38
+ export declare function flattenRichtext(value: unknown, maxDepth?: number): unknown;
39
+ /**
40
+ * Per-itemtype whitelists. Derived from real payloads: what is dropped for
41
+ * Ticket is the SLA/OLA block, the *_delay_stat counters and the HAL `links`.
42
+ */
43
+ export declare const ESSENTIAL_FIELDS: Record<string, string[]>;
44
+ export declare function pickFields(itemtype: string | undefined, obj: Record<string, unknown>, mode: FieldMode): Record<string, unknown>;
45
+ /**
46
+ * Formats a whole payload. Objects and arrays of objects are filtered;
47
+ * anything else is returned untouched.
48
+ */
49
+ export declare function formatPayload(itemtype: string | undefined, data: unknown, mode: FieldMode): unknown;
50
+ /**
51
+ * Renders rows as a markdown table. Columns come from the union of the keys
52
+ * present, in first-seen order, so a sparse row does not lose its data.
53
+ * Long cells are cut at 80 characters: a table is for scanning a listing;
54
+ * open the item (or ask for JSON) to read a long text in full.
55
+ */
56
+ export declare function toMarkdownTable(rows: Record<string, unknown>[]): string;
57
+ /**
58
+ * One column of a conversational markdown view: `label` heads the column and
59
+ * `from` says where the value comes from — a key, a list of candidate keys
60
+ * (the first non-empty one wins) or a function of the row.
61
+ */
62
+ export interface MarkdownColumn {
63
+ label: string;
64
+ from: string | readonly string[] | ((row: Record<string, unknown>) => unknown);
65
+ }
66
+ /** Projects a full row onto the columns of a view. */
67
+ export type MarkdownView = (row: Record<string, unknown>) => Record<string, unknown>;
68
+ /**
69
+ * Builds a view from a column list. A listing in markdown is read in a
70
+ * conversation: the 22 columns of a raw ticket row (bare IDs such as
71
+ * `users_id_lastupdater: 368` among them) bury the 8 a person scans for.
72
+ * The JSON result keeps every field; only the markdown table is projected.
73
+ */
74
+ export declare function columnView(columns: readonly MarkdownColumn[]): MarkdownView;
75
+ /** Renders a payload as markdown; falls back to JSON for non-tabular shapes. */
76
+ export declare function renderMarkdown(data: unknown): string;