Skip to main content
Glama

idm-mcp

MCP server for an ISOTEL IDM smart gateway. It exposes the gateway's devices, parameters, records, protocol stacks, streams and connections to an MCP client (Claude Desktop, Chatbox, an agent, anything speaking MCP).

Read-only by default - 14 tools that only GET. Writing is opt-in: --allow-write registers the write tools, IDM_WRITABLE names the parameters that may be changed, and every value is validated against the gateway's own options (read-only flag, allowed values, numeric range) before anything is sent.

Built on isotel-idm and fastmcp; target gateway API 1.3.845.

Workflow

  1. idm_list_devices — what is attached.

  2. idm_get_device_html for any device you are about to read or change. It is the description layer: sections (System, Acquire, Trigger, Channels, ...), units, live values and free-text diagnostics, with every value tagged by its parameter name. It honours the access level, so it grows from 140 to 195 parameters with --enable-devel --enable-hidden.

  3. Then the value tools.

Skip step 2 only when the user has already named a specific parameter and value — the intent is unambiguous then. Otherwise describe first and let the sections inform which parameters matter; don't infer meaning from a parameter name alone. The write tools return device_described and a hint when it is false.

Related MCP server: Remote Link Local MCP

Tools

Tool

Returns

idm_gateway_info()

host, API version, mode, group, active and known device counts

idm_list_devices(include_inactive=False, brand_filter=None)

[{name, device, root, status}]

idm_device_status(device)

state, port, endpoints, time, recording, counters

idm_get_protocols(device, full=False)

protocol stack layers, plus streams with tcp_port / gateway_host

idm_get_streams(device=None)

every TCP stream with its name, port, clients, rx/tx

idm_list_connections()

every PHY connection with state and link counters, including one with no device

idm_get_device_html(device, max_bytes=8192)

the gateway's own HTML page for a device — the description step; honours the access level

idm_get_activity(limit=20)

recent gateway activity log — the receipt for a write

idm_list_parameters(device, prefix=None, fresh=False)

parameter paths only — for discovery

idm_get_parameters(device, prefix=None, names=None, fresh=False)

flattened {"ec/E": {"value", "unit"}}

idm_get_structure(device, path="", options=False, fresh=False)

a whole substructure in one request, e.g. cfg or ch/PGA_in

idm_get_value(device, parameter, fresh=False, options=False)

one parameter: value, unit, accuracy, timestamp; with options=True also type / allowedValues / readOnly

idm_get_history(device, parameter, time_from, time_to, max_points, scale, average, calc, interpolate)

recorded samples, aggregated by the gateway

device accepts the root (device0), the full name (spaces or underscores) or an unambiguous substring. Ambiguity is refused with the candidate list rather than silently picking the first device. Parameter paths use slashes (ec/E); dots (ec.E) are accepted too.

idm_list_parameters reports variables of a device that wraps them in a data container as data/ch/PGA_in/PGA_in0, but the gateway addresses that variable as ch/PGA_in/PGA_in0. The tools accept either and strip the container, so paths from idm_list_parameters can be used as-is. prefix filters match through the container too: prefix="sys" selects data/sys/uptime.

Every value carries timestamp, the gateway's UTC time of the sample — compare it across polls before believing that anything changed.

History modes

The gateway does the aggregation, so a month or a day costs one call:

Arguments

Mode

Result

max_points=N

regular

N samples (limit): first N after time_from, or last N before time_to; gateway default 100

scale=N

scale

N points spanning the span, each with m = records in its interval

scale=N, average=3600

average

N points averaged over 3600 s intervals, with n and sd

calc=avg

calc

one aggregate over the whole record set (n_sum = inputs)

average is an interval duration in seconds — the Python library's docstring calls it a record count, which is wrong. max_points and scale are mutually exclusive; average and interpolate require scale.

Streams

idm_get_protocols flattens the protocol stack of the port a device sits on: PHYSICAL -> VIRTUALPHY -> MESSAGE -> CUSTOM, with each layer's rx/tx counters. A layer that carries a tcp_port is a stream served by the gateway — connect to <gateway_host>:<tcp_port> for the raw byte stream:

"streams": [{"name": "UART 115200-8E1 to Amplifier", "layer": "CUSTOM",
             "tcp_port": 33100, "gateway_host": "gateway", "clients": []}]

Only active devices have a connection port, so this needs one. full=True returns the tree exactly as the gateway sends it.

Write mode

Read-only is the default and the write tools are then not registered at all. Enable them with --allow-write (alias --allow-post) or IDM_ALLOW_WRITE=1:

python -m idm_mcp.server --transport http --port 8003 --allow-write

Tool

Effect

idm_command(command, parameters)

sends a terminal command (POST /api/v1/terminal/commands)

idm_open_stream(device, stream)

gateway dials the stream: openTCPConnection(host, port)

idm_close_stream(device, stream)

closes it: closeConnection("host:port")

idm_open_serial(target, preset=None)

opens a serial port or preset (openSerialPort)

idm_close_serial(connection_id)

closes a serial connection by id (closeConnection)

idm_set_value(device, parameter, value)

writes one parameter — only if allow-listed (see below)

idm_set_structure(device, values, path="")

writes several parameters in one POST — the struct idm_get_structure returns, minus status/time

Opening a stream makes the gateway connect to it. idm_open_stream sends openTCPConnection(host, port); the gateway dials its own endpoint and creates a PHY connection named <host>:<port>. If a device stack sits behind that stream, it loads as a new device — the stacking case: opening TestPort's UART 115200-8E1 to Amplifier brings up a second IOLite-4CFB- (same serial) over that UART, and idm_close_stream (closeConnection("host:port")) unloads it again. The host defaults to localhost because the gateway dials itself; override with IDM_STREAM_HOST. idm_read_stream is the different operation: it attaches from this host to read raw bytes, and detaches afterwards.

Every write returns its activity entries. The gateway's response body is just {"result": "OK"|"Error"|"null"}, so a command's real outcome comes from GET /me/activity: the client snapshots the newest log timestamp before the POST and returns entries logged after it. A Command not found or Exception while executing entry makes the stream/connection tools raise instead of reporting a silent no-op.

Command names carry no $ prefix on the wire — help, not $help.

Device parameter writes are deliberately not implemented: they need an explicit allowlist of parameters and are not part of this surface yet.

Access level

The gateway hides whole sections behind three access flags, and the client passes your setting both when it opens the device and on every read, so the level can never differ between one call and the next:

python -m idm_mcp.server --enable-advanced --enable-hidden --enable-devel

Flag

Env

Reveals

--enable-advanced

IDM_ADVANCED

advanced sections

--enable-hidden

IDM_HIDDEN

hidden sections — cfg (rsv, release) needs this

--enable-devel (alias --enable-development)

IDM_DEVELOPMENT

development sections — a substructure GET like /ch.json is refused without it

All three are off by default, which is the gateway's own narrow view. On device4 the default shows 142 parameters and cfg comes back empty; with all three it shows 195 and cfg returns {"rsv": "0x28", "release": "2"}.

Parameter writes

Two write paths, same allowlist and same validation: idm_set_value for one variable, idm_set_structure for a whole section in a single POST.

Substructures keep their wrapper key. get("") on ch/PGA_in answers {"status", "PGA_in": {...}, "time"}. Send that back minus status and time, wrapper included. An unwrapped payload is accepted and wrapped automatically, because the gateway otherwise applies the first value to every sibling — all four channels took the same gain when I tried it. After the POST the tool re-reads the section and returns verified plus mismatch, so a write that did not take is visible rather than assumed.

idm_set_value refuses everything unless the parameter is on an allowlist you define, and the allowlist lives outside the code:

IDM_WRITABLE="device4:ch/PGA_in/PGA_in0,device4:ch/ADC/ADC0,*:ch/range/*" \
IDM_ALLOW_WRITE=1 pm2 restart idm-mcp --update-env

Each entry is device:parameter where device is a root (device4), a full name, or *; parameter is a path, optionally ending in * for a prefix. An empty allowlist means nothing is writable.

Validation before the POST uses the gateway's own options for that parameter (read them with idm_get_value(..., options=True)):

  • readOnly: true → refused;

  • enum → the value must be in allowedValues, matched case-insensitively and sent in canonical spelling (vin → Vin);

  • numeric → must parse, and respect min/max when the gateway reports them.

The payload is the gateway's own shape, {"<variable>": {"value": "..."}}, POSTed to the parameter's URL.

Freshness

idm_get_value, idm_get_parameters, idm_get_structure and idm_list_parameters return the gateway's last stored sample unless fresh=True, which appends after=now to the GET and makes the gateway query the device (one round-trip over USB/Bluetooth, so slower). Use it on every poll whose point is to observe change — the gateway will otherwise happily serve the same cached sample indefinitely.

Install

pip install -e . --no-deps

--no-deps because isotel-idm and fastmcp are already in that venv.

Run

stdio — the client spawns the process:

~/.venv/bin/idm-mcp                      # or: python -m idm_mcp.server

HTTP — long-running, e.g. under pm2:

python -m idm_mcp.server --transport http --host 127.0.0.1 --port 8003 --path /mcp

Ends up at http://127.0.0.1:8003/mcp.

Environment

Variable

Default

Meaning

IDM_GATEWAY

http://localhost:33000

gateway base URL

IDM_GROUP

me

group (me = local devices)

IDM_TIMEOUT

60

request timeout, seconds

IDM_USERNAME / IDM_PASSWORD

unset

basic auth, if the gateway needs it

IDM_ALLOW_WRITE

unset (off)

register the write tools (1/true/yes/on)

IDM_WRITABLE

unset (nothing)

allowlist of device:parameter entries for the write tools

IDM_ADVANCED / IDM_HIDDEN / IDM_DEVELOPMENT

unset (off)

access level; same as --enable-*

IDM_STREAM_HOST

localhost

host the gateway dials for idm_open_stream (it connects to its own endpoint)

IDM_OPEN_SERIAL_CMD / IDM_CLOSE_SERIAL_CMD

openSerialPort / closeConnection

command names used by idm_open_serial / idm_close_serial, for firmware that spells them differently

IDM_MCP_TRANSPORT / IDM_MCP_HOST / IDM_MCP_PORT / IDM_MCP_PATH

stdio / 127.0.0.1 / 8003 / /mcp

transport defaults

IDM_PYTHON

python3

interpreter used by ecosystem.config.cjs (pm2 only)

IDM_LIVE

unset

1 runs the live tests against IDM_GATEWAY

Command line

Every setting can also be given as a flag; a flag wins over its environment variable.

Flag

Equivalent

--gateway URL

IDM_GATEWAY

--group NAME

IDM_GROUP

--timeout SEC

IDM_TIMEOUT

--username / --password

IDM_USERNAME / IDM_PASSWORD

--allow-write (alias --allow-post)

IDM_ALLOW_WRITE=1

--enable-advanced

IDM_ADVANCED=1

--enable-hidden

IDM_HIDDEN=1

--enable-devel (alias --enable-development)

IDM_DEVELOPMENT=1

--transport stdio|http

IDM_MCP_TRANSPORT

--host / --port / --path

IDM_MCP_HOST / IDM_MCP_PORT / IDM_MCP_PATH

IDM_WRITABLE deliberately has no flag: an allowlist belongs in one place, not on a command line where it can be mistyped or silently lost from the pm2 environment.

CLI flags override the environment.

Client configuration (stdio)

{
  "mcpServers": {
    "isotel-idm": {
      "command": "/path/to/venv/bin/idm-mcp",
      "env": { "IDM_GATEWAY": "http://localhost:33000", "IDM_GROUP": "me" }
    }
  }
}

Protocol era (mode)

FastMCP 4.x negotiates one of two protocol eras per connection: legacy (initialize, 2025-11-25) or the modern server/discover era (2026-07-28). mode is a client-side setting — Client(..., mode="auto"|"legacy"|version); the server has no equivalent switch and answers whichever era the client asks for.

Both work against this server over stdio, verified with fastmcp 4.1.2: mode="auto" settles on 2026-07-28, mode="legacy" on 2025-11-25, both list 14 tools and execute calls. So legacy is not required — if a client's auto fails here, that is its probe, not this server: auto sends server/discover first and falls back to initialize only for an RPC error, so a client that times the probe out or that predates the fallback reports a connection failure while legacy still succeeds.

pm2

pm2 start ecosystem.config.cjs
pm2 logs idm-mcp
pm2 restart idm-mcp
pm2 delete idm-mcp
IDM_ALLOW_WRITE=1 IDM_GATEWAY=http://gateway:33000 pm2 restart idm-mcp --update-env

ecosystem.config.cjs runs the HTTP transport on 127.0.0.1:8003/mcp with autorestart. It uses __dirname for its working directory and takes the interpreter from IDM_PYTHON (default python3 on PATH), so start it with the environment that has isotel-idm and fastmcp installed:

IDM_PYTHON=/path/to/venv/bin/python pm2 start ecosystem.config.cjs

Test

~/.venv/bin/python -m pytest              # 111 unit tests, no gateway needed
IDM_LIVE=1 ~/.venv/bin/python -m pytest   # + 6 tests against the real gateway
~/.venv/bin/python scripts/smoke_http.py  # exercises a running HTTP server

tests/test_server.py fails if any write operation (set, post, delete, user/group management, scripts) appears in the package source, and if the tool set changes — read-only is a tested property, not a promise.

Behaviour worth knowing

  • Only active devices serve live telemetry. idm_get_history also accepts inactive devices, because the gateway keeps their records. A gateway lists inactive entries for devices it knows but is not running (they have no root), and those entries mirror active devices, so active always wins in resolution.

  • Device objects are cached per name, and one Group keeps a keep-alive connection; the client is thread-safe. Device construction redirects stdout, because isotel.idm prints an inactive-device warning that would corrupt the JSON-RPC stream on the stdio transport.

  • isotel-idm pins the exact API revision (1.3.845). A gateway firmware update that bumps the revision makes the library refuse to connect; upgrading isotel-idm is then required.

  • Not included: service/user/group management, script execution, virtual devices, firmware upgrade and show/aggr group-link vertical aggregation.

Requirements

Python 3.10+, fastmcp, and isotel-idm 1.3 for a gateway running API 1.3. The library enforces an API major-version match when it opens the group, so isotel-idm 1.2 installs fine but refuses to connect to a 1.3 gateway (Major API versions not compatible, gateway: 1.3 library: 1.2).

PyPI currently publishes up to 1.2, so the dependency is declared >=1.2: that keeps pip install -e . valid while 1.3 is distributed privately — an existing 1.3 install already satisfies the requirement and pip will not downgrade it. The pin moves to >=1.3 once 1.3 is on PyPI.

License

LGPL-3.0-only, matching isotel-idm. See LICENSE.

Available Tools

14 tools
idm_device_statusIdm Device StatusC

Connection state of one device: state, port, endpoints, last update time.

Inputs: device (required). Returns: object

idm_gateway_infoIdm Gateway InfoA

Describe the gateway: host, API version, mode, group and device count.

Inputs: no named parameters. Returns: object

idm_get_activityIdm Get ActivityA

Read the gateway's recent activity log.

This is the real receipt for a write: the gateway reports rejected commands here as "Command not found: ...".

Inputs: limit. Returns: object

idm_get_device_htmlIdm Get Device HtmlA

Start here: fetch the gateway's own HTML page for a device.

This is the description layer. It groups parameters into sections (System, Acquire, Trigger, Channels, ...), shows units, live values and free-text diagnostics (status, message, error number), and every value carries its parameter name - so it is how you learn what a device is and what its parameters mean before reading or writing them.

It honours the server's access level: with development/hidden enabled the page grows (140 -> 195 parameters on this gateway). Fetching it marks the device as described, which the write tools report back.

Inputs: device (required), max_bytes. Returns: object

idm_get_historyIdm Get HistoryA

Read recorded samples of one parameter, aggregated by the gateway.

Four modes, chosen by which arguments are set: regular (default) - max_points samples from the span. scale - exactly scale points spanning the span, each with m records. average - scale plus average: interval duration in SECONDS; adds n, sd. calc - one aggregate over the whole record set, e.g. "avg".

Also works for inactive devices: the gateway keeps their records.

Inputs: device (required), parameter (required), calc, scale, +5 more. Returns: object

idm_get_parametersIdm Get ParametersB

Read current values of a device's parameters, flattened to path -> {value, unit}.

Inputs: device (required), fresh, names, prefix. Returns: object

idm_get_protocolsIdm Get ProtocolsA

Show the protocol and stream stack of the port a device is attached to.

The ISN stack is a compound object tree: MESSAGE / TRANSPORT / FRAME / USER / CUSTOM layers can nest to any depth, and new protocol types appear without notice. Layers are returned flattened, each with its path. A layer carrying a tcp_port is a stream: connect to <gateway_host>:<tcp_port> for the raw byte stream.

Inputs: device (required), full. Returns: object

idm_get_streamsIdm Get StreamsB

List the TCP streams the gateway exposes, with their names.

De-duplicated across devices that share one connection port.

Inputs: device. Returns: object

idm_get_structureIdm Get StructureA

Fetch a whole sub-structure in one request (get("") on that path).

Use this instead of idm_get_parameters when you want the gateway's own nested JSON for one section, e.g. "cfg" or "ch/PGA_in", including hidden/development sections.

Inputs: device (required), path, options. Returns: object

idm_get_valueIdm Get ValueC

Read a single parameter, e.g. device="device0", parameter="ec/E".

Inputs: device (required), parameter (required), fresh, options. Returns: object

idm_list_connectionsIdm List ConnectionsA

List the gateway's physical connections with link counters.

Shows every connection the gateway holds - USB, serial or a TCP client it dialled for a stream - including one with no device behind it, which the device and stream views cannot represent.

Inputs: no named parameters. Returns: object

idm_list_devicesIdm List DevicesC

List the devices of this group.

Inputs: brand_filter, include_inactive. Returns: object

idm_list_parametersIdm List ParametersA

List a device's parameter paths without their values.

Inputs: device (required), prefix. Returns: object

idm_read_streamIdm Read StreamA

Read raw bytes from a stream.

Attaches to the stream for the read and detaches again unless it was already opened with idm_open_stream. Returns the bytes as hex and text.

Inputs: device (required), stream (required), seconds, max_bytes. Returns: object

Tool Schema Changelog

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

Loading tool schema changelog…

    TDQS

    B3.4/5.0

    Scored across 14 tools

    Disambiguation4/5

    Most tools target distinct resources (devices, connections, streams, activity, history), and the parameter-reading family (list_parameters, get_parameters, get_value, get_structure) is differentiated by descriptions. However, the four parameter tools plus the topology trio (get_protocols, get_streams, list_connections) require careful reading to avoid misselection.

    Naming Consistency4/5

    All tools share the idm_ prefix and follow a mostly consistent verb_noun pattern (list_devices, get_parameters, read_stream, get_history). Minor deviations: idm_gateway_info and idm_device_status are noun-only, and get vs read both appear but are used sensibly.

    Tool Count5/5

    14 tools is well-scoped for a gateway/device management server, covering topology, parameters, streams, and history without obvious redundancy. Each tool earns its place.

    Completeness3/5

    The read surface is thorough (info, devices, connections, streams, parameters, history), but the descriptions reference write tools and idm_open_stream that are not present in the tool list, implying missing write capability. The surface is read-only despite describing a read/write parameter workflow.

    Maintenance

    ActivityMaintained
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers