Files

220 lines
9.1 KiB
Markdown

# 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=<app token>
export MGLPI_USER_TOKEN=<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) |
`MGLPI_URL` is the FULL API base INCLUDING `/apirest.php` — e.g.
`https://cmdb.knownelement.com/apirest.php`, not just the server host.
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: <sentinel>: 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", "<p>content</p>", 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