Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 49 additions & 6 deletions INSTRUCTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This directory contains the MCP servers and infrastructure for the AssetOpsBench
- [Quick Start](#quick-start)
- [Environment Variables](#environment-variables)
- [MCP Servers](#mcp-servers) — full reference in [docs/mcp-servers.md](docs/mcp-servers.md)
- [Python client (mcphub)](#python-client-mcphub)
- [Example queries](#example-queries)
- [Agents](#agents)
- [Observability](#observability)
Expand Down Expand Up @@ -150,6 +151,52 @@ Tool signatures, required env vars, and how to launch a server directly: **[docs

---

## Python client (mcphub)

`src/mcphub/` calls the MCP tools directly from Python — the same servers the
agents use, but without an LLM in the loop. It exposes a ToolUniverse-style
`load_tools → run` interface and adds no dependencies beyond the `mcp` client the
project already requires.

```python
from mcphub import ToolUniverse

tu = ToolUniverse() # 1. init
tu.load_tools() # 2. connect + discover
result = tu.run({ # 3. run
"name": "iot.sensors",
"arguments": {"site_name": "MAIN", "asset_id": "Chiller 6"},
})
tu.close()
```

- Tools are namespaced `<server>.<tool>` (e.g. `iot.sensors`,
`wo.generate_work_order`); a bare name works when unambiguous.
- `load_tools(servers=["iot", "fmsr"])` limits the set; no args loads all six.
- Discovery: `tu.find_tools("failure mode")`, `tu.list_tools()`,
`tu.tool_specification("iot.sensors")`.
- Servers are spawned via `uv run <server>-mcp-server` and inherit your `.env`.
Run inside the project (`uv run python ...`) or an activated venv. Override the
launch table with `ToolUniverse(servers={...})`.

**Workflows** — multi-tool routines are plain functions registered under a name
and invoked through the same entrypoint:

```python
tu.run({"name": "chiller_triage", "arguments": {"asset_id": "Chiller 6"}})
```

Built in: `chiller_triage` (sensors → failure modes → mapping → work order) and
`sensor_inventory_gap` (installed vs measured sensors). Add your own in
`src/mcphub/workflows.py`. Runnable example:
`uv run python examples/quickstart_tooluniverse.py`.

Reach for `mcphub` when you want deterministic, scripted tool orchestration
(pipelines, tests, data prep); use the agent runners when you want LLM-driven
planning.

---

## Example queries

The CLI examples below use a `$query` shell variable so you can swap in any question without editing the commands. Pick one of these to get started:
Expand Down Expand Up @@ -381,12 +428,8 @@ uv run pytest src/ -k "integration" # only files / tests with "in

```
┌────────────────────────────────────────────────────────────────────────┐
│ agent/ │
│ │
│ PlanExecuteRunner ClaudeAgentRunner StirrupAgentRunner │
│ OpenAIAgentRunner DeepAgentRunner OpenCodeAgentRunner │
│ DirectLLMAgentRunner │
│ │
│ agent runners mcphub (Python client) │
│ PlanExecute · Claude · OpenAI · Deep · Stirrup · OpenCode · DirectLLM │
└──────────────────────────────┬─────────────────────────────────────────┘
│ MCP protocol (stdio)
┌─────────────────────┼───────────┬──────────┬──────┬───────────┐
Expand Down
51 changes: 51 additions & 0 deletions docs/tool_universe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Three-step contract (same as ToolUniverse)

```python
from mcphub import ToolUniverse

tu = ToolUniverse() # 1. init
tu.load_tools() # 2. load (connect + discover)
tu.run({ # 3. run
"name": "iot.sensors",
"arguments": {"site_name": "MAIN", "asset_id": "Chiller 6"},
})
tu.close()
```

`load_tools(servers=[...])` limits to specific servers. Tools are namespaced
`<server>.<tool>`; a bare name (e.g. `sensors`) also works when unambiguous.
A shorthand `tu.run("iot.sensors", {...})` is accepted too.

## Discovery

```python
tu.find_tools("failure mode") # keyword search over loaded tools
tu.list_tools() # all loaded tool + workflow names
tu.list_tools("fmsr") # tool names for one server
tu.tool_specification("iot.sensors")
```

## Workflows

Composed workflows run through the **same `run` entrypoint**:

```python
tu.run({"name": "chiller_triage", "arguments": {"asset_id": "Chiller 6"}})
```

Add one by writing `fn(tu, **arguments)` in `workflows.py` and listing its name
in `REGISTERED` (or `tu.register_workflow("name", fn)` at runtime). Built in:
`chiller_triage` (sensors → failure modes → mapping → work order) and
`sensor_inventory_gap` (installed vs measured sensors).

## Run

```bash
uv sync
docker compose -f src/couchdb/docker-compose.yaml up -d # iot / wo data
cp .env.public .env # WATSONX_* for fmsr
uv run python examples/quickstart_tooluniverse.py
```

Each server launches via `uv run <server>-mcp-server` and inherits your
environment. Override with `ToolUniverse(servers={...})` if you run outside `uv`.
43 changes: 43 additions & 0 deletions examples/quickstart_tooluniverse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""
Quick start — ToolUniverse-style interface. Prereqs (repo root):
uv sync
docker compose -f src/couchdb/docker-compose.yaml up -d
cp .env.public .env # WATSONX_* for fmsr
Run:
uv run python examples/quickstart_tooluniverse.py
"""
import json

from mcphub import ToolUniverse


def show(t, o):
print(f"\n=== {t} ===")
print(json.dumps(o, indent=2, default=str))


def main():
tu = ToolUniverse() # 1. init
try:
tu.load_tools(servers=["iot", "fmsr", "wo"]) # 2. load (connect + discover)

show("find 'failure mode'",
[s["name"] for s in tu.find_tools("failure mode")])

# 3. run a single tool (ToolUniverse dict form)
show("iot.sensors", tu.run({
"name": "iot.sensors",
"arguments": {"site_name": "MAIN", "asset_id": "Chiller 6"},
}))

# A workflow runs through the same entrypoint
show("chiller_triage", tu.run({
"name": "chiller_triage",
"arguments": {"asset_id": "Chiller 6", "raise_work_order": False},
}))
finally:
tu.close()


if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/agent", "src/evaluation", "src/llm", "src/observability", "src/servers"]
packages = ["src/agent", "src/evaluation", "src/llm", "src/observability", "src/servers", "src/mcphub"]

[project]
name = "assetopsbench-mcp"
Expand Down
Loading