Skip to content

Architecture

Rod Christiansen edited this page Oct 8, 2026 · 2 revisions

Architecture

How the server is put together and why it is shaped the way it is. The whole implementation is about 200 lines across five files.

Modules

Module Responsibility
config.py Config, a frozen dataclass. from_env() validates and resolves REPORTMATE_API_URL plus one credential; auth_header() picks the header.
client.py ReportMateClient, a wrapper around an httpx.AsyncClient with the base URL, auth header, Accept, and User-Agent baked in. One method: get(path, params), which raises on non-2xx and returns parsed JSON.
server.py The FastMCP("reportmate") instance, the MODULES tuple, and the eight @mcp.tool functions.
__init__.py __version__, the calendar version literal.
__main__.py main() — calls mcp.run(). Registered as the reportmate-mcp console script.

Request flow

A tool call travels one hop and comes back:

agent -> MCP client -> stdio -> FastMCP dispatch -> tool function
      -> ReportMateClient.get() -> HTTPS GET /api/v1/... -> ReportMate API

The tool functions do almost nothing beyond building a path. The single exception is search_devices, which filters and truncates in Python after fetching the whole device list — see Tools Reference for what that means in practice.

Lazy client construction

server.py holds a module-level _client that starts as None. The client() helper builds it on first use:

def client() -> ReportMateClient:
    global _client
    if _client is None:
        _client = ReportMateClient(Config.from_env())
    return _client

That is deliberate — the module imports cleanly with no environment set, which is what lets the test suite import server and enumerate tools without credentials. The cost is that a bad configuration surfaces as a failed tool call rather than a failed startup.

The client is created once and reused for the process lifetime, so httpx keeps the connection pool warm across tool calls. aclose() exists on the client but nothing in the server calls it; the process exiting is the cleanup.

The MODULES tuple

server.py defines the ten collection modules that match the dashboard tabs:

inventory  installs  applications  system  management
identity   hardware  peripherals   security  network

It is documentation for the agent, not a validator — no tool checks its argument against it. module_report and get_device_module pass anything through, which is how nested variants such as installs/full reach the API through module_report. get_device_module takes only a single module segment upstream. An unrecognized module produces an upstream HTTP error, not a local one.

Design decisions

Read-only. Only GET is reachable; ReportMateClient has no post, put, or delete method. Archive and delete endpoints exist on the API and are deliberately not registered. Adding them would mean adding both a client method and an explicit opt-in.

Intent-shaped, not endpoint-shaped. Eight tools cover a much larger API surface. An agent reasons better about a small, well-named toolset than about a faithful mirror of every route, and each extra tool competes for attention in the model's context.

Thin. No caching, no retries, no response reshaping, no pagination handling. Whatever the API returns is what the agent sees. This keeps the server honest about being a transport for the API rather than a second implementation of it — but it does mean fleet-wide tools can return large payloads.

Single-tenant per process. Credentials come from the process environment, so one running server is one fleet with one identity. Multiple fleets means multiple server entries in your client config.

Packaging

Hatchling builds a wheel from src/reportmate_mcp, with the version read from the __version__ literal in __init__.py. The Dockerfile is a python:3.12-slim base that copies pyproject.toml, README.md, and src, runs pip install ., and sets reportmate-mcp as the entry point. Environment variables are supplied at run time; nothing is baked into the image.

Clone this wiki locally