9.1 KiB
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
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
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/AddFollowupparse 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 carriesSession-Token;killSessioncleans up (the client re-inits transparently afterwards). - Profiles:
GET /getMyProfileslists the user's profiles;POST /changeActiveProfilewith{"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, notErrValidation.
Quickstart (verified against the fake)
This is the exact flow the smoke run (./dev.sh smoke) executes:
./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)
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).
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
./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