Skip to main content
Glama
cmms-mcp-server

cmms-mcp-server

ci

cmms-mcp-server

An MCP server that lets an AI assistant work with a maintenance management system (CMMS): look up assets, read and acknowledge alarms, and create and move work orders through a real life cycle, with role checks and an audit trail.

Everything runs on fictional data (a made-up plant with Quanta compressors, Ironwood presses, Helix lathes, Nimbus chillers and Vortex pumps; 16 assets, 12 alarms, 9 work orders, 5 users). There is no connection to any real system.

AI client  --MCP/stdio-->  tools.py (schemas, annotations, error mapping)
                              |
                          service.py (validation, roles, state machine, transactions)
                              |
                          SQLite (db.py)

The domain logic in service.py knows nothing about MCP, so it is tested directly, and the MCP layer is tested separately, in-process and over a real stdio subprocess.

Run it

uv sync
uv run cmms-mcp-server                                        # stdio, in-memory, reseeded each start
uv run cmms-mcp-server --db plant.sqlite3                     # persistent (created and seeded when missing)
uv run cmms-mcp-server --read-only                            # write tools are not registered at all
uv run cmms-mcp-server --fixed-now 2026-10-09T06:00:00Z       # freeze the clock (the seed is set on that day)
uv run python examples/demo_client.py                         # scripted client session, see below

Add it to an MCP client with .mcp.json.example (command uv run --directory ... cmms-mcp-server). SKILL.md is a companion skill that teaches an assistant the order of calls and the safety rules.

Related MCP server: PRAETOR-MCP

Tools

tool

kind

what it does

get_plant_overview

read

counts of assets/alarms/work orders, overdue work, class-A assets with unacknowledged alarms

list_assets

read

filter by status, type, location, criticality, free text; paged

get_asset

read

one asset + its uncleared alarms, active work orders, last completed work order

list_alarms

read

filter by state, severity, asset, time; most severe first

list_work_orders

read

filter by status, priority, asset, assignee, overdue; highest priority first

get_work_order

read

one work order with its full event history

acknowledge_alarm

write, idempotent

take ownership of an alarm; repeating it changes nothing

create_work_order

write

optionally linked to an alarm; one active work order per alarm

assign_work_order

write

supervisors only

transition_work_order

write

the life cycle below, with role and note rules

Also: resource cmms://glossary and prompt shift_handover.

Work-order life cycle:

open -> assigned -> in_progress <-> on_hold -> completed -> closed
  \        \                          \          \-> in_progress   (reopen, supervisor)
   `--------`--------------------------`-> cancelled

role

may

viewer

read

technician

acknowledge, create, start / hold / complete work assigned to them

supervisor

everything: assign, unassign, close, cancel, reopen, act for anyone

A note (5+ characters) is required to hold, complete, cancel, unassign or reopen.

What a session looks like

Real output of examples/demo_client.py (long values shortened by the script):

> acknowledge_alarm as a viewer (must fail)
  ERROR Error executing tool acknowledge_alarm: forbidden: view.d is a viewer and cannot acknowledge alarms

> acknowledge_alarm as tech.a
  {"alarm": {"id": "ALM-0001", "asset_id": "AST-0001", "severity": "critical", "code": "OIL_TEMP_HIGH", ...

> create_work_order
  {"work_order": {"id": "WO-2026-0010", "asset_id": "AST-0001", "title": "Oil temperature high: inspect cooler", ...

> create_work_order again for the same alarm (must fail)
  ERROR Error executing tool create_work_order: conflict: alarm ALM-0001 already has an active work order: WO-2026-0010

> transition straight to completed (must fail)
  ERROR Error executing tool transition_work_order: invalid: cannot move a work order from 'open' to 'completed'; allowed: assigned, cancelled

Design decisions

  • Errors are written for a model. A domain error becomes a tool error code: reason that says what was wrong and what is allowed ("allowed: assigned, cancelled"). Unexpected exceptions stay generic: the SDK hides their text, and a test checks that a crash message containing a path does not reach the client.

  • Typed outputs. Tools return pydantic models, so each has an output schema and clients get structuredContent. A test fails if any tool or parameter lacks a description.

  • Annotations. Reads are marked readOnlyHint; acknowledge_alarm and assign_work_order are idempotent; nothing is destructive (records are never deleted).

  • --read-only removes the write tools instead of rejecting calls, so the model never sees them.

  • Duplicates are prevented where retries happen. A model may call create_work_order twice; the second call for the same alarm is a conflict that names the existing work order.

  • Writes are all-or-nothing (BEGIN IMMEDIATE). A test makes the event insert fail and checks that the status update is rolled back too.

  • Injection and wildcards. All SQL is parameterised; % and _ in search text are escaped.

  • Bounded output. Every list has limit (1-100) and next_offset.

  • Injectable clock (--fixed-now) so "overdue" and ids are deterministic in tests and demos.

Tests

74 test cases (uv run pytest): state machine; every read filter and ordering; every write rule and permission; rollback; tool inventory, annotations, schemas, documentation of every parameter; error mapping; read-only mode; resource and prompt; and a full triage flow plus persistence across two server processes over real stdio.

Mutation check: 20 deliberate bugs (viewer allowed to acknowledge, duplicate guard removed, ownership check dropped, no rollback, unescaped LIKE, crash text leaking, write tools registered in read-only mode, terminal state with an exit, ...) were each caught by at least one test. One mutant first survived because it was not actually leaking; it was rewritten and then caught.

Limitations

  • No authentication. actor is a claimed identity checked against a user table. That is fine for a demo and for stdio on your own machine; a real deployment must take the identity from the transport (for example OAuth on streamable HTTP) and never from a tool argument. The streamable-http option exists but is unauthenticated.

  • One process, one SQLite connection with a lock. No multi-writer support, no migrations.

  • Offset paging: rows added between pages can shift the window.

  • Not tested against a real assistant (Claude Desktop, Claude Code, ...); it is tested with the MCP SDK's own client. SKILL.md is guidance I wrote and have not measured.

  • Written for mcp 2.x (MCPServer, from mcp.client.client import Client); version 1.x used FastMCP and different client entry points, hence the mcp>=2.2,<3 pin.

Development

uv sync
uv run pytest
uv run ruff check

License

MIT, see LICENSE.

Available Tools

10 tools
acknowledge_alarmA
Idempotent

Take ownership of an active alarm. Safe to repeat: an already acknowledged alarm is returned unchanged with already_acknowledged=true.

Inputs: actor (required), alarm_id (required). Returns: alarm, already_acknowledged

assign_work_orderA
Idempotent

Assign (or reassign) an open or assigned work order. Supervisors only.

Inputs: actor (required), assignee (required), work_order_id (required). Returns: events, work_order

create_work_orderA

Create a new work order (status open). Check list_work_orders for the asset first to avoid duplicates.

Inputs: actor (required), title (required), asset_id (required), wo_type, +4 more. Returns: events, work_order

get_assetA
Read-onlyIdempotent

One asset with its uncleared alarms, its active work orders and its last completed work order.

Inputs: asset_id (required). Returns: asset, active_alarms, open_work_orders, last_completed_work_order

get_plant_overviewA
Read-onlyIdempotent

Counts of assets, active alarms and work orders, overdue work, and critical (A) assets with unacknowledged alarms. Call this first.

Inputs: no named parameters. Returns: as_of, assets_by_status, overdue_work_orders, work_orders_by_status, +2 more

get_work_orderB
Read-onlyIdempotent

One work order with its full history of events (who did what, when, and why).

Inputs: work_order_id (required). Returns: events, work_order

list_alarmsB
Read-onlyIdempotent

List alarms, most severe and most recent first.

Inputs: limit, since, state, offset, +2 more. Returns: items, total, next_offset

list_assetsA
Read-onlyIdempotent

List assets, most critical first. All filters are optional and combine with AND.

Inputs: type, limit, query, offset, +3 more. Returns: items, total, next_offset

list_work_ordersB
Read-onlyIdempotent

List work orders, highest priority first, then earliest due date.

Inputs: limit, offset, status, asset_id, +3 more. Returns: items, total, next_offset

transition_work_orderA

Move a work order along its life cycle. The assignee or a supervisor can start, hold and complete; only a supervisor can close, cancel, unassign or reopen. Use assign_work_order for open -> assigned.

Inputs: actor (required), to_status (required), work_order_id (required), note. Returns: events, work_order

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.8/5.0

    Scored across 10 tools

    Disambiguation4/5

    Most tools target a clearly distinct resource+action (overview vs list vs get vs mutate). The only real overlap is assign_work_order vs transition_work_order, but descriptions explicitly carve out the boundary ('Use assign_work_order for open -> assigned'), which mostly resolves it.

    Naming Consistency5/5

    Every tool follows a clean verb_noun snake_case pattern (get_plant_overview, list_assets, get_asset, acknowledge_alarm, create_work_order, assign_work_order, transition_work_order). No casing or verb-style deviations.

    Tool Count5/5

    10 tools is well within the ideal 3-15 range and each one maps to a distinct CMMS capability (dashboard, asset/alarm/WO reads, and key WO/alarm mutations). Nothing feels padded or missing on count grounds.

    Completeness3/5

    Work order lifecycle is well covered (create, assign, transition, history), but there are notable gaps: get_asset references 'uncleared' alarms yet no tool clears/resolves an alarm, and there is no way to update a work order or append notes/log entries. Asset creation/editing is also absent, leaving some dead ends.

    Maintenance

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      C
      quality
      B
      maintenance
      Reference implementation of the Operational Ontology pattern: MCP tools are generated from a typed business domain model (objects, links, actions) — one tool per query shape and per action, deliberately no raw SQL tool. Writes pass business-rule preconditions, are audited, and write back to the systems of record; refusals are machine-readable. Ships with a demo orders ontology spanning two legacy
      34
      203
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      Provides a local synthetic MCP stdio service for querying maintenance evidence and preparing human-review-gated advisory packets, with deterministic governance, evidence boundary tracing, and Agent K integrity scoring to separate retrieved evidence from model inference while preserving human review authority.
      11 npm
      MIT
    • F
      license
      Not graded
      quality
      D
      maintenance
      Enables AI agents to generate audit reports and query financial ledger data via MCP. This demo version contains intentional security vulnerabilities for security testing and should not be used in production.
      -