This directory contains the MCP servers and infrastructure for the AssetOpsBench project.
- Prerequisites
- Quick Start
- Environment Variables
- MCP Servers
- Plan-Execute Runner
- Connect to Claude Desktop
- Running Tests
- Architecture
-
Python 3.12+ — required by
pyproject.toml -
uv — dependency and environment manager
curl -LsSf https://astral.sh/uv/install.sh | sh # macOS / Linux # or: brew install uv
-
Docker — for running CouchDB (IoT data store)
Run from the repo root:
uv syncuv sync creates a virtual environment at .venv/, installs all dependencies, and registers the CLI entry points (plan-execute, *-mcp-server). You can either prefix commands with uv run (no activation needed) or activate the venv once for your shell session:
source .venv/bin/activate # macOS / LinuxCopy .env.public to .env and fill in the required values (see Environment Variables):
cp .env.public .env
# Then edit .env and set WATSONX_APIKEY, WATSONX_PROJECT_ID
# CouchDB defaults work out of the box with the Docker setupdocker compose -f src/couchdb/docker-compose.yaml up -dVerify CouchDB is running:
curl -X GET http://localhost:5984/Note: MCP servers use stdio transport — they are spawned on-demand by clients (Claude Desktop,
plan-execute) and exit when the client disconnects. They are not long-running daemons.
To start a server manually for testing:
uv run utilities-mcp-server
uv run iot-mcp-server
uv run fmsr-mcp-server
uv run tsfm-mcp-server
uv run wo-mcp-server
uv run vibration-mcp-serverCouchDB — iot and wo servers
| Variable | Default | Description |
|---|---|---|
COUCHDB_URL |
http://localhost:5984 |
CouchDB connection URL |
COUCHDB_USERNAME |
admin |
CouchDB admin username |
COUCHDB_PASSWORD |
password |
CouchDB admin password |
IOT_DBNAME |
chiller |
IoT sensor database name |
WO_DBNAME |
workorder |
Work order database name |
VIBRATION_DBNAME |
vibration |
Vibration sensor database name |
WatsonX — plan-execute runner (when --model-id starts with watsonx/)
| Variable | Default | Description |
|---|---|---|
WATSONX_APIKEY |
(required) | IBM WatsonX API key |
WATSONX_PROJECT_ID |
(required) | IBM WatsonX project ID |
WATSONX_URL |
https://us-south.ml.cloud.ibm.com |
WatsonX endpoint (optional) |
LiteLLM — plan-execute runner (when --model-id does not start with watsonx/, e.g. litellm_proxy/…)
| Variable | Default | Description |
|---|---|---|
LITELLM_API_KEY |
(required) | LiteLLM proxy API key |
LITELLM_BASE_URL |
(required) | LiteLLM proxy base URL, e.g. https://your-litellm-host.example.com |
Path: src/servers/iot/main.py
Requires: CouchDB (COUCHDB_URL, COUCHDB_USERNAME, COUCHDB_PASSWORD, IOT_DBNAME)
| Tool | Arguments | Description |
|---|---|---|
sites |
— | List all available sites |
assets |
site_name |
List all asset IDs for a site |
sensors |
site_name, asset_id |
List sensor names for an asset |
history |
site_name, asset_id, start, final? |
Fetch historical sensor readings for a time range (ISO 8601 timestamps) |
Path: src/servers/utilities/main.py
Requires: nothing (no external services)
| Tool | Arguments | Description |
|---|---|---|
json_reader |
file_name |
Read and parse a JSON file from disk |
current_date_time |
— | Return the current UTC date and time as JSON |
current_time_english |
— | Return the current UTC time as a human-readable string |
Path: src/servers/fmsr/main.py
Requires: WATSONX_APIKEY, WATSONX_PROJECT_ID, WATSONX_URL for unknown assets; curated lists for chiller and ahu work without credentials.
Failure-mode data: src/servers/fmsr/failure_modes.yaml (edit to add/change asset entries)
| Tool | Arguments | Description |
|---|---|---|
get_failure_modes |
asset_name |
Return known failure modes for an asset. Uses a curated YAML list for chillers and AHUs; falls back to the LLM for other types. |
get_failure_mode_sensor_mapping |
asset_name, failure_modes, sensors |
For each (failure mode, sensor) pair, determine relevancy via LLM. Returns bidirectional fm→sensors and sensor→fms maps plus full per-pair details. |
Path: src/servers/wo/main.py
Requires: CouchDB (COUCHDB_URL, COUCHDB_USERNAME, COUCHDB_PASSWORD, WO_DBNAME)
Data init: Handled automatically by docker compose -f src/couchdb/docker-compose.yaml up (runs src/couchdb/init_wo.py inside the CouchDB container on every start — database is dropped and reloaded each time)
| Tool | Arguments | Description |
|---|---|---|
get_work_orders |
equipment_id, start_date?, end_date? |
Retrieve all work orders for an equipment within an optional date range |
get_preventive_work_orders |
equipment_id, start_date?, end_date? |
Retrieve only preventive (PM) work orders |
get_corrective_work_orders |
equipment_id, start_date?, end_date? |
Retrieve only corrective (CM) work orders |
get_events |
equipment_id, start_date?, end_date? |
Retrieve all events (work orders, alerts, anomalies) |
get_failure_codes |
— | List all failure codes with categories and descriptions |
get_work_order_distribution |
equipment_id, start_date?, end_date? |
Count work orders per (primary, secondary) failure code pair, sorted by frequency |
predict_next_work_order |
equipment_id, start_date?, end_date? |
Predict next work order type via Markov transition matrix built from historical sequence |
analyze_alert_to_failure |
equipment_id, rule_id, start_date?, end_date? |
Probability that an alert rule leads to a work order; average hours to maintenance |
Path: src/servers/tsfm/main.py
Requires: tsfm_public (IBM Granite TSFM), transformers, torch for ML tools — imported lazily; static tools work without them.
Model checkpoints: resolved relative to PATH_TO_MODELS_DIR (default: src/servers/tsfm/artifacts/output/tuned_models)
| Tool | Arguments | Description |
|---|---|---|
get_ai_tasks |
— | List supported AI task types for time-series analysis |
get_tsfm_models |
— | List available pre-trained TinyTimeMixer (TTM) model checkpoints |
run_tsfm_forecasting |
dataset_path, timestamp_column, target_columns, model_checkpoint?, forecast_horizon?, frequency_sampling?, ... |
Zero-shot TTM inference; returns path to a JSON predictions file |
run_tsfm_finetuning |
dataset_path, timestamp_column, target_columns, model_checkpoint?, save_model_dir?, n_finetune?, n_test?, ... |
Few-shot fine-tune a TTM model; returns saved checkpoint path and metrics file |
run_tsad |
dataset_path, tsfm_output_json, timestamp_column, target_columns, task?, false_alarm?, ad_model_type?, ... |
Conformal anomaly detection on top of a forecasting output JSON; returns CSV with anomaly labels |
run_integrated_tsad |
dataset_path, timestamp_column, target_columns, model_checkpoint?, false_alarm?, n_calibration?, ... |
End-to-end forecasting + anomaly detection in one call; returns combined CSV |
Path: src/servers/vibration/main.py
Requires: CouchDB (COUCHDB_URL, VIBRATION_DBNAME (default vibration), COUCHDB_USERNAME, COUCHDB_PASSWORD); numpy, scipy
DSP core: src/servers/vibration/dsp/ — adapted from vibration-analysis-mcp (Apache-2.0)
| Tool | Arguments | Description |
|---|---|---|
get_vibration_data |
site_name, asset_id, sensor_name, start, final? |
Fetch vibration time-series from CouchDB and load into the analysis store. Returns a data_id. |
list_vibration_sensors |
site_name, asset_id |
List available sensor fields for an asset. |
compute_fft_spectrum |
data_id, window?, top_n? |
Compute FFT amplitude spectrum (top-N peaks + statistics). |
compute_envelope_spectrum |
data_id, band_low_hz?, band_high_hz?, top_n? |
Compute envelope spectrum for bearing fault detection (Hilbert transform). |
assess_vibration_severity |
rms_velocity_mm_s, machine_group? |
Classify vibration severity per ISO 10816 (Zones A–D). |
calculate_bearing_frequencies |
rpm, n_balls, ball_diameter_mm, pitch_diameter_mm, contact_angle_deg?, bearing_name? |
Compute bearing characteristic frequencies (BPFO, BPFI, BSF, FTF). |
list_known_bearings |
— | List all bearings in the built-in database. |
diagnose_vibration |
data_id, rpm?, bearing_designation?, bearing_*?, bpfo_hz?, bpfi_hz?, bsf_hz?, ftf_hz?, machine_group?, machine_description? |
Full automated diagnosis: FFT + shaft features + bearing envelope + ISO 10816 + fault classification + markdown report. |
src/workflow/ is a custom MCP client that implements a plan-and-execute workflow over the MCP servers. It replaces AgentHive's bespoke orchestration with the standard MCP protocol.
PlanExecuteRunner.run(question)
│
├─ 1. Discover query each MCP server for its available tools
│
├─ 2. Plan LLM decomposes the question into ordered steps,
│ each assigned to an MCP server
│
├─ 3. Execute for each step (in dependency order):
│ • LLM selects the right tool + generates arguments
│ • tool is called via MCP stdio protocol
│ • result is stored and passed as context to later steps
│
└─ 4. Summarise LLM synthesises step results into a final answer
After uv sync, the plan-execute command is available:
uv run plan-execute "What assets are available at site MAIN?"Note:
plan-executespawns MCP servers on-demand for each query — you do not need to start them manually first. Servers are launched as subprocesses, used, then exit automatically.
Flags:
| Flag | Description |
|---|---|
--model-id MODEL_ID |
litellm model string with provider prefix (default: watsonx/meta-llama/llama-4-maverick-17b-128e-instruct-fp8) |
--server NAME=SPEC |
Override MCP servers with NAME=SPEC pairs (repeatable); SPEC is an entry-point name or path |
--show-plan |
Print the generated plan before execution |
--show-history |
Print each step result after execution |
--json |
Output answer + plan + history as JSON |
The provider is encoded in the --model-id prefix:
| Prefix | Provider | Required env vars |
|---|---|---|
watsonx/ |
IBM WatsonX | WATSONX_APIKEY, WATSONX_PROJECT_ID, WATSONX_URL (optional) |
litellm_proxy/ |
LiteLLM proxy | LITELLM_API_KEY, LITELLM_BASE_URL |
Examples:
# WatsonX — default model
uv run plan-execute "What assets are at site MAIN?"
# WatsonX — different model, inspect the plan
uv run plan-execute --model-id watsonx/ibm/granite-3-3-8b-instruct --show-plan "List sensors for asset CH-1"
# LiteLLM proxy
uv run plan-execute --model-id litellm_proxy/GCP/claude-4-sonnet "What are the failure modes for a chiller?"
# Machine-readable output
uv run plan-execute --show-history --json "How many observations exist for CH-1?" | jq .answerAll six servers (iot, utilities, fmsr, tsfm, wo, vibration) are registered by default.
Equipment IDs in the sample dataset: CWC04014 (524 WOs), CWC04013 (431 WOs), CWC04009 (alert events).
# Work order count and most common failure code
uv run plan-execute "How many work orders does equipment CWC04014 have, and what is the most common failure code?"
# Preventive vs corrective split
uv run plan-execute "For equipment CWC04013, how many preventive vs corrective work orders were completed?"
# Alert-to-failure probability
uv run plan-execute "What is the probability that alert rule RUL0018 on equipment CWC04009 leads to a work order, and how long does it typically take?"
# Work order distribution + next prediction (multi-step)
uv run plan-execute --show-plan --show-history \
"For equipment CWC04014, show the work order distribution and predict the next maintenance type"Run a question that exercises three servers with independent parallel steps:
uv run plan-execute --show-plan --show-history \
"What is the current date and time? Also list assets at site MAIN. Also get failure modes for a chiller."Expected plan (3 parallel steps, no dependencies):
[1] utilities : current_date_time()
[2] iot : assets(site_name="MAIN")
[3] fmsr : get_failure_modes(asset_name="chiller")
Expected execution output (trimmed):
[OK] Step 1 (utilities)
{"currentDateTime": "2026-02-20T17:28:39Z", "currentDateTimeDescription": "Today's date is 2026-02-20 and time is 17:28:39."}
[OK] Step 2 (iot)
{"site_name": "MAIN", "total_assets": 1, "assets": ["Chiller 6"], "message": "found 1 assets for site_name MAIN."}
[OK] Step 3 (fmsr)
{"asset_name": "chiller", "failure_modes": ["Compressor Overheating: Failed due to Normal wear, overheating", ...]}
Note: Curated assets (
chiller,ahu) are served fromfailure_modes.yamlwithout any LLM call.
import asyncio
from workflow import PlanExecuteRunner
from llm import LiteLLMBackend
runner = PlanExecuteRunner(llm=LiteLLMBackend("watsonx/meta-llama/llama-3-3-70b-instruct"))
result = asyncio.run(runner.run("What assets are available at site MAIN?"))
print(result.answer)OrchestratorResult fields:
| Field | Type | Description |
|---|---|---|
answer |
str |
Final synthesised answer |
plan |
Plan |
The generated plan with its steps |
history |
list[StepResult] |
Per-step execution results |
Implement LLMBackend to use any model:
from llm import LLMBackend
class MyLLM(LLMBackend):
def generate(self, prompt: str, temperature: float = 0.0) -> str:
... # call your model here
runner = PlanExecuteRunner(llm=MyLLM())Pass server_paths to register additional servers. Keys must match the server names the planner assigns steps to:
from workflow import PlanExecuteRunner
runner = PlanExecuteRunner(
llm=my_llm,
server_paths={
"iot": "iot-mcp-server",
"utilities": "utilities-mcp-server",
"fmsr": "fmsr-mcp-server",
"tsfm": "tsfm-mcp-server",
"wo": "wo-mcp-server",
"vibration": "vibration-mcp-server",
},
)Note: passing
server_pathsreplaces the defaults entirely. Include all servers you need.
Add the following to your Claude Desktop claude_desktop_config.json:
{
"mcpServers": {
"utilities": {
"command": "/path/to/uv",
"args": [
"run",
"--project",
"/path/to/AssetOpsBench",
"utilities-mcp-server"
]
},
"iot": {
"command": "/path/to/uv",
"args": ["run", "--project", "/path/to/AssetOpsBench", "iot-mcp-server"]
},
"fmsr": {
"command": "/path/to/uv",
"args": ["run", "--project", "/path/to/AssetOpsBench", "fmsr-mcp-server"]
},
"tsfm": {
"command": "/path/to/uv",
"args": ["run", "--project", "/path/to/AssetOpsBench", "tsfm-mcp-server"]
},
"wo": {
"command": "/path/to/uv",
"args": ["run", "--project", "/path/to/AssetOpsBench", "wo-mcp-server"]
},
"vibration": {
"command": "/path/to/uv",
"args": ["run", "--project", "/path/to/AssetOpsBench", "vibration-mcp-server"]
}
}
}Run the full suite from the repo root (unit + integration where services are available):
uv run pytest src/ -vIntegration tests are auto-skipped when the required service is not available:
- IoT integration tests require
COUCHDB_URL(set in.env) - Work order integration tests require
COUCHDB_URL(set in.env) - FMSR integration tests require
WATSONX_APIKEY(set in.env) - TSFM integration tests require
PATH_TO_MODELS_DIRandPATH_TO_DATASETS_DIR(set in.env)
uv run pytest src/ -v -k "not integration"uv run pytest src/servers/iot/tests/test_tools.py -k "not integration"
uv run pytest src/servers/utilities/tests/
uv run pytest src/servers/fmsr/tests/ -k "not integration"
uv run pytest src/servers/tsfm/tests/ -k "not integration"
uv run pytest src/servers/wo/tests/test_tools.py -k "not integration"
uv run pytest src/workflow/tests/docker compose -f src/couchdb/docker-compose.yaml up -d
uv run pytest src/servers/wo/tests/test_integration.py -vdocker compose -f src/couchdb/docker-compose.yaml up -d
uv run pytest src/ -v┌──────────────────────────────────────────────────────┐
│ workflow/ │
│ │
│ PlanExecuteRunner.run(question) │
│ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │
│ │ Planner │ → │ Executor │ → │ Summariser │ │
│ │ │ │ │ │ │ │
│ │ LLM breaks │ │ Routes each│ │ LLM combines │ │
│ │ question │ │ step to the│ │ step results │ │
│ │ into steps │ │ right MCP │ │ into answer │ │
│ └────────────┘ │ server via │ └──────────────┘ │
│ │ stdio │ │
└───────────────────┼────────────┼─────────────────────┘
│ MCP protocol (stdio)
┌──────────┼──────────┬──────────┬──────┬───────────┐
▼ ▼ ▼ ▼ ▼ ▼
iot utilities fmsr tsfm wo vibration
(tools) (tools) (tools) (tools) (tools) (tools)