# mopac-glpi-go A 100% Go client library, CLI (`mglpi`), and MCP server (`mglpi-mcp`) for the GLPI REST API (ITSM/CMDB). Replaces python glue for every CMDB update: one static binary, zero third-party modules (stdlib only), machine-parseable JSON output, and tokens that never appear in flags, logs, or error strings. Clones the architecture of [mopac-redmine-go](https://git.knownelement.com/ukrrs/mopac-redmine-go) file for file. Status: 2026-09-03 — v0 complete and green: changes (create / show / list / transition / followup), CI search + raw item fetch, profiles + agent-profile auto-switch, session lifecycle (init / kill), 0600 env-file config, `-o json` on every command, exit codes 0/1/2 with one-line API-error stderr, and a stdio MCP server (initialize / tools-list / 6 tools). Built and tested entirely against a fake GLPI (unit suite + end-to-end smoke); the live CMDB (cmdb.knownelement.com) attaches with zero code change via `MGLPI_URL`/`MGLPI_APP_TOKEN`/`MGLPI_USER_TOKEN`. [#767] ## Architecture ```mermaid flowchart LR subgraph host[Host] M[mglpi CLI\nbin/mglpi] --config 0600 env --> M P[mglpi-mcp\nstdio JSON-RPC] --> M S[smoke/smoke.sh] end subgraph docker[Digest-pinned Docker builder — ALL dev] B[go build / vet / test] F[smoke/fakeglpi\nstateful in-memory GLPI] end subgraph wire[Library: package glpi] C[Client\nApp-Token + Session-Token headers only] end M --> C P --> C C --> F C -.-> G[(real GLPI\ncmdb.knownelement.com)] S --> B S --> F ``` All compilation, vetting, and testing runs inside the digest-pinned `golang:1.26-bookworm` builder (`./dev.sh`); the host never runs a Go toolchain. Tests and smoke talk only to the in-process/containerized fake; the real CMDB is never contacted by the test suite. ## GLPI quirks modeled (and tested) - **Create endpoints return an ARRAY**: `[{"id":N,"message":"..."}]` — never an object. `CreateChange` / `AddFollowup` parse it. - **Input shapes differ per endpoint**: Change create takes `{"input":{...}}` (object); ITILFollowup REQUIRES `{"input":[{...}]}` (array of objects) — the fake rejects the wrong shape with a 400, and the client sends each faithfully. - **Search rows are keyed by field-id string** under forcedisplay: `{"data":[{"1":"name","2":7,"12":3}]}` — parsed into typed rows (fields 1 name, 2 id, 12 status). - **Sessions**: `POST /initSession` (App-Token + `Authorization: user_token ...`) returns `{"session_token":"..."}`; every later call carries `Session-Token`; `killSession` cleans up (the client re-inits transparently afterwards). - **Profiles**: `GET /getMyProfiles` lists the user's profiles; `POST /changeActiveProfile` with `{"profiles_id":N}` switches the session's active profile. Agent mode uses this to move into the right role (e.g. 5 = Hotliner) before working. - **initSession auth failures are auth failures**: GLPI answers 400 (ERROR_GLPI_LOGIN / ERROR_APP_TOKEN_...) for bad credentials — the client maps that to `ErrAuth`, not `ErrValidation`. ## Quickstart (verified against the fake) This is the exact flow the smoke run (`./dev.sh smoke`) executes: ```sh ./dev.sh check # build + vet + test inside the Docker builder ./dev.sh smoke # boots fake GLPI on 127.0.0.1:8602, drives bin/mglpi # against the real CMDB instead (0600 env file, see env.example): export MGLPI_URL=https://cmdb.knownelement.com/apirest.php export MGLPI_APP_TOKEN= export MGLPI_USER_TOKEN= mglpi whoami # profiles, [active] marker mglpi change create --title "Disk pressure" --urgency 4 <<'EOF' - array05 on pfv-tsys5 at 92% EOF mglpi change list --status new mglpi change show 104 mglpi change transition 104 solved mglpi change followup 104 <<'EOF' REPORT delivered: array expanded EOF mglpi ci search Computer web mglpi ci show Computer 101 -o json ``` Agent mode: put `MGLPI_PROFILE_ID=5` in the env file (or pass `--profile 5`) — the CLI switches to that profile right after InitSession, so profile-gated servers accept the calls. `whoami` never switches. Add `-o json` to any command for machine output. ## Command reference | Command | Effect | Key flags | |---|---|---| | `whoami` | the session's GLPI profiles | `-o json` | | `change create --title T` | create change (body from stdin) | `--content FILE\|-`, `--urgency 1..5`, `--impact 1..5`, `--profile N`, `-o json` | | `change show ID` | one change with content | `-o json` | | `change list` | search (forcedisplay 1/2/12) | `--status N\|NAME`, `-o json` | | `change transition ID STATUS` | move status (name or number) | `-o json` (re-fetches) | | `change followup ID` | append ITILFollowup (body from stdin) | `--content FILE\|-`, `--profile N` | | `ci search TYPE TERM` | name-substring search | `-o json` | | `ci show TYPE ID` | raw CI object | `-o json` (default shape) | | `help` | usage | | Flags may appear before or after positionals (`mglpi change show 104 -o json` parses). ## Configuration | Source | Keys | Discipline | |---|---|---| | environment | `MGLPI_URL`, `MGLPI_APP_TOKEN`, `MGLPI_USER_TOKEN`, `MGLPI_PROFILE_ID` (optional) | env wins over file | | `--config PATH` (also `$MGLPI_CONFIG`, default `~/.config/mglpi/env`) | same keys | file must be 0600 or stricter; refused before any read; parsed in pure Go (no sourcing/expansion) | Tokens travel only in headers: `App-Token` on every request, `Authorization: user_token ...` on initSession, `Session-Token` after. Response bodies are never surfaced in error strings — the fake GLPI deliberately echoes the presented token in error bodies, and the test suite proves nothing leaks (`TestTokensNeverLeak`, plus the smoke redaction pass). ## Exit codes and stderr | Code | Meaning | stderr shape | |---|---|---| | 0 | ok | — | | 1 | usage / config error | one line, names the problem (never values) | | 2 | API error | one line: `mglpi: glpi: : http NNN` — parseable | Sentinels (library): `glpi.ErrAuth`, `ErrNotFound`, `ErrValidation`, `ErrServer`, `ErrUnreachable`, `ErrMalformedResponse` — all comparable with `errors.Is`. ## Library surface (what the harness calls) ```go import "git.knownelement.com/ukrrs/mopac-glpi-go/glpi" c := glpi.New(glpi.Config{BaseURL: url, AppToken: app, UserToken: user}) _ = c.InitSession(ctx) // also lazy on first call _ = c.ChangeActiveProfile(ctx, 5) // agent mode (5 = Hotliner) profs, _ := c.GetMyProfiles(ctx) // id/name/is_active id, _ := c.CreateChange(ctx, "title", "

content

", 3, 4) ch, _ := c.GetChange(ctx, id) // full read shape rows, _ := c.ListChanges(ctx, glpi.StatusNew) // search, fields 1/2/12 _ = c.TransitionChange(ctx, id, glpi.StatusSolved) _ = c.AddFollowup(ctx, id, "note") // {"input":[{...}]} shape cis, _ := c.SearchCI(ctx, "Computer", "web") obj, _ := c.GetItem(ctx, "Computer", 101) // raw map for ci show _ = c.KillSession(ctx) ``` Status helpers: `glpi.StatusNew`…`StatusClosed` constants, `StatusName(id)`, `StatusID(name)`. All methods take a `context.Context`. ## MCP server `cmd/mglpi-mcp` is a minimal stdio JSON-RPC MCP server (stdlib only): `initialize` (echoes protocol_version + serverInfo), `tools/list` (six tools), `tools/call`, `ping`. Tool failures are results with `isError: true`; only unknown methods are JSON-RPC errors (-32601). ```sh docker exec -i mopac-cli-mglpi mglpi-mcp # from the compose exec-host printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}' \ | mglpi-mcp --config ~/.creds/mglpi.env ``` Tools: `change_create`, `change_list`, `change_transition`, `change_followup`, `ci_search`, `ci_show`. ## Development ```sh ./dev.sh build|vet|test|check|smoke|mglpi|mcp|shell # everything runs in the digest-pinned builder make build|vet|test|check|smoke # same, Makefile front door ``` Test suite: table-driven round-trips against the stateful fake (`internal/fakeglpi`): session/header discipline, create/array-reply parsing, keyed search rows, transition, followup input-shape contract, CI search, profiles + the RequireProfile rejection path (agent mode), error mapping (400/401/403/404/422/5xx → sentinels), unreachable/malformed handling, and token redaction. Shell scripts are shellcheck-clean (including info level). ## Status | Area | State | |---|---| | Library (changes, followups, search, profiles, sessions) | done, green | | CLI surface (`mglpi`) | done, green | | MCP server (`mglpi-mcp`, 6 tools) | done, green (handshake + calls tested) | | Fake-GLPI test suite | done, green (unit + smoke) | | Token redaction | done, tested (fake echoes all three tokens; nothing leaks) | | Dev harness (Docker-only builds) | done (`dev.sh`/`Makefile`) | | Live-CMDB verification | pending first live attach via `MGLPI_URL`/`MGLPI_APP_TOKEN`/`MGLPI_USER_TOKEN` | License: AGPLv3 (see `LICENSE`). Pattern source: https://git.knownelement.com/ukrrs/mopac-redmine-go