Skip to main content
Glama
DPBeahr-rok

acm_mcp

by DPBeahr-rok
README.md
# acm_mcp

MCP server for building PLC automation project plans using the Rockwell Automation **ACM (Application Code Manager)** library catalog.

An LLM client connects to this server to browse the ACM catalog, resolve linked library dependencies, and assemble a project hierarchy — controllers, tasks, programs, routines, and AOI definitions — with automatic placement rules and deduplication.

## Quick Start

```bash
cd acm_mcp
uv sync                        # install dependencies (creates .venv)
uv run python server.py        # start server on port 8012
```

The server loads the library catalog from `data/data.db` on startup and exposes tools over **streamable-http** at `http://127.0.0.1:8012/mcp`.

## Configuration

All settings are in `config.yaml`:

```yaml
server:
  host: "127.0.0.1"
  port: 8012
  log_level: "INFO"
  transport: "streamable-http"
  ssl_certfile: ""             # path to TLS cert for HTTPS (optional)
  ssl_keyfile: ""              # path to TLS key for HTTPS (optional)

acm_browser:
  working_dir: "data"        # relative to this directory
  pull_libraries: false      # set true to re-export from ACM
  pull_templates: false
  pull_database: false

resolver:
  max_depth: 10              # linked library recursion limit
```

## Populating the Database

The server relies on a SQLite database (`data/data.db`) built from ACM library exports. On startup, it reads the three `pull_*` flags in `config.yaml` to decide whether to re-run each step of the pipeline:

| Flag | What it does | ACM Browser call |
|------|-------------|-----------------|
| `pull_libraries` | Runs ACMConsole to export registered libraries (HSL4 files) into the working directory | `ExportRegisteredLibraries()` |
| `pull_templates` | Converts exported HSL4 files into XML templates using `ACMTemplateGen.CLI.exe` | `ParseHSL4IntoTemplate()` |
| `pull_database` | Clears and rebuilds `data.db` by parsing templates, extracting parameters, decoding PDF docs, and linking everything | `fillDatabase()` |

### First-time setup (no existing data.db)

Set all three flags to `true` in `config.yaml` to run the full pipeline:

```yaml
acm_browser:
  working_dir: "data"
  pull_libraries: true
  pull_templates: true
  pull_database: true
```

Then start the server:

```bash
uv run python server.py
```

This will export libraries from ACM, convert them to templates, and populate `data.db`. The process requires ACM to be installed on the machine.

### Subsequent runs (data.db already exists)

Set all flags to `false` to skip re-export and reuse the existing database:

```yaml
acm_browser:
  working_dir: "data"
  pull_libraries: false
  pull_templates: false
  pull_database: false
```

This is the default configuration and makes startup much faster.

### Refreshing the catalog

To pick up newly registered ACM libraries, set all three flags back to `true` and restart the server. You can also selectively re-run steps — for example, set only `pull_database: true` if you've manually updated template files and just need to rebuild the database.

## Tools

| # | Tool | Description |
|---|------|-------------|
| 1 | `resolve_library` | Preview a library's dependencies before adding it. Returns tiered results: tier_1_mandatory (REQUIRED, AOI_AUTO_ADDED, DEDUP, SELECT), tier_2_optional (user decides), tier_3_conditional (conditionals, SELECTs). |
| 2 | `search_libraries` | Search the catalog by text query, LibraryType, Category, or ContentType. |
| 3 | `fuzzy_search_libraries` | Fuzzy name search across the catalog. Use when you have an approximate or misspelled name. Returns results ranked by similarity score. |
| 4 | `list_controllers` | List all available controller types (plain and MachineBuilder variants). |
| 5 | `add_controller` | Add a controller as the root node. Accepts optional `parameter_overrides` (JSON string). MachineBuilder controllers auto-create pre-built tasks. |
| 6 | `add_task` | Create an empty task under a controller (PERIODIC, EVENT, or CONTINUOUS). |
| 7 | `add_program` | Create an empty program under a task. |
| 8 | `add_item` | Add a library item with automatic placement. AOI dependencies are auto-registered; all other dependencies appear in `todo` for explicit linking via `resolve_link`. |
| 9 | `resolve_link` | Bind an unresolved dependency on a node to a specific instance in the project. Replaces `resolve_select` — works with all link types (REQUIRED, SELECT, CONDITIONAL-MET, OPTIONAL). |
| 10 | `get_unresolved_links` | List all unresolved dependency links across the entire project, with existing instances that match each link's candidates. |
| 11 | `remove_item` | Remove a node and all children (cascade). Automatically unresolves any links on remaining nodes that pointed to a removed node, restoring them to their original tag. Reports broken links in the response. |
| 12 | `get_project_views` | Render the project as two text trees: Class View (by type) and Preview (hierarchy). |
| 13 | `get_project_state` | Get the full project state as structured JSON. |
| 14 | `rename_instance` | Rename any node in the project. |
| 15 | `set_project_name` | Set the project name (used as filename on export). |
| 16 | `export_project` | Save the project state to `projects/{name}/{name}.json`. |
| 17 | `load_project` | Load a previously saved project from disk. |
| 18 | `list_projects` | List all saved project files. |
| 19 | `instantiate_in_acm` | Generate ACM XML scripts from an exported project JSON and invoke ACMConsole to instantiate the project in ACM. Uses separate BEGINCREATE/ENDCREATE blocks for project, controller, and objects so the controller's pre-built tasks are fully instantiated before objects reference them. Pre-built items (CLX auto-created tasks/programs) are excluded from the objects XML — their parent HSL4 definitions create them automatically. Does not generate an ACD file — use `export_acd` for that. |
| 20 | `export_acd` | Export the ACM project to an ACD file. Must be called after `instantiate_in_acm`. |
| 21 | `reset_project` | Reset the project, clearing all in-memory state. Use `export_project` first to save work. |

## Typical Workflow

1. Server injects planning context automatically via FastMCP `instructions` (loaded from `prompts/context_injection.md`)
2. Client clarifies vague requests before making tool calls — asks the user what they want, where it goes, and any configuration choices
3. Client searches the catalog with `search_libraries`, `fuzzy_search_libraries`, or `list_controllers` — presents results to user, never assumes first match
4. Client calls `resolve_library` to inspect dependencies before committing
5. Client adds a controller with `add_controller`
6. Client creates tasks/programs with `add_task` / `add_program` (required for plain controllers; MachineBuilder controllers come with pre-built tasks)
7. Client adds library items with `add_item` — placement is automatic, AOI dependencies are auto-registered
8. Client presents `todo` items to the user for each `add_item` response — never silently resolves links
9. Client adds all needed items (in any order), then resolves dependencies with `resolve_link` after presenting binding choices to the user
10. Client calls `get_unresolved_links` to see all pending dependencies across the project
11. Client calls `get_project_views` after each change to show the user
12. Client calls `export_project` to save (blocks if mandatory links are unresolved)
13. Optionally, client calls `instantiate_in_acm` to instantiate the project in ACM, then `export_acd` to generate an ACD file

## Project Structure

```
acm_mcp/
├── server.py                  # MCP server entry point (21 tools)
├── config.yaml                # Server and ACM configuration
├── pyproject.toml             # uv project definition
├── uv.lock                   # Locked dependencies
├── .python-version            # Python 3.13
├── models/
│   ├── library.py             # Library, PlacementRole, LinkedLibraryRef
│   └── project_state.py       # InstanceNode, ProjectState tree
├── services/
│   ├── library_store.py       # In-memory catalog with search indexes
│   ├── resolver.py            # Recursive linked library resolution
│   ├── project_manager.py     # Stateful project builder + export/load
│   └── renderer.py            # Class View and Preview tree rendering
├── prompts/
│   └── context_injection.md   # LLM planning context (loaded as FastMCP instructions)
├── data/
│   ├── data.db                # SQLite catalog (524 libraries)
│   ├── Templates/             # 524 Template.xml source files
│   └── instance_templates/    # XML templates for ACD generation (project, controller, object)
└── projects/                  # Saved project files (JSON)
```

## Key Concepts

- **Routine-First Thinking**: The routine is the fundamental working piece — the function call that invokes the AOI (the function definition). When a user asks to "add a conveyor," start by finding the routine and work outward to program, task, and controller.
- **PlacementRole**: Determined by ContentType + LibraryType. Controls where items go in the hierarchy (controller, task, program, routine, AOI).
- **Linked Library Resolution**: Recursive (max depth 10) with explicit, generic (wildcard), template, and conditional resolution modes. Recursion only enters AOI_AUTO_ADDED dependencies — REQUIRED items get their own resolution when manually added, preventing transitive dependencies from leaking onto parent nodes as duplicate ILLib entries.
- **Dependency Tags**: Each dependency gets one tag: `AOI_AUTO_ADDED`, `DEDUP`, `REQUIRED`, `OPTIONAL`, `CONDITIONAL-MET`, `CONDITIONAL-SKIP`, `CONDITIONAL-UNKNOWN`, `SELECT`, `TEMPLATE`, `NOT-FOUND`, or `RESOLVED`. AOIs are auto-handled; all others require explicit user decisions.
- **Instance-Level Binding**: Links bind to specific project instances via `target_node_id`, not catalog numbers. This supports many-to-many scenarios (e.g., multiple Equipment Modules linking to different Unit Modules).
- **No Auto-Resolver**: Dependencies are not silently resolved. The agent must present choices to the user and call `resolve_link` explicitly. Items can be added in any order.
- **AOI Deduplication**: Asset-Control items are globally deduplicated — one definition serves all instances. Tier 1 and tier 3 condition-met AOIs are auto-registered; tier 2 AOIs require user confirmation.
- **Cascade Removal with Link Cleanup**: `remove_item` deletes the target node and all its children, then walks the remaining project tree to find any resolved links that pointed to a removed node. Those links are automatically unresolved and restored to their original tag (e.g., SELECT, REQUIRED). Broken links are reported in the response. Orphaned AOI definitions are not auto-removed — they must be cleaned up manually if no longer needed.
- **Parameter Overrides**: Both `add_controller` and `add_item` accept `parameter_overrides` (JSON string). You may include parameters not present in the library definition — ACM will accept them if the names are valid. **Parameter names are case-sensitive** and must match exactly; incorrect casing causes ACM to silently ignore the parameter.
- **MachineBuilder Controllers**: Come with pre-built periodic tasks at various scan rates. Guard variants include a SafetyTask.
- **CLX Auto-Created Children**: Some libraries (e.g., `raM_Robot_Dvc_DeviceHandler`) define CLX children — tasks and programs that ACM's HSL4 engine creates automatically during instantiation. These are flagged `is_prebuilt=True` in the project state and excluded from the objects XML to avoid conflicts with unresolvable substitution tokens (e.g., `{MotionGroupName}`, `{RbtDvcItf$RobotName}`). ACM resolves these tokens internally through its interface link chain.
- **ACM Console Script Structure**: The instantiation script uses separate BEGINCREATE/ENDCREATE blocks for project, controller, and objects. This ensures the controller's HSL4 fully executes (creating pre-built tasks like `ms0008p08`, `PowerUp_Handler`, `SafetyTask`) before CREATEOBJECTS runs, allowing objects to reference those tasks as ParObj/Task.
- **Export Ordering**: The `catalog_items` in exported JSON preserve insertion order (the order items were added by the user), not alphabetical order. This has no effect on ACM instantiation — ACM parses all objects into an insert list before processing — but reflects the logical build sequence.
- **State**: In-memory during the session. Use `export_project` / `load_project` to persist across sessions.

## Client Configuration

```json
{
  "mcpServers": {
    "acm_mcp": {
      "command": "npx.cmd",
      "args": [
        "mcp-remote",
        "http://127.0.0.1:8012/mcp",
        "--allow-http"
      ]
    }
  }
}
```

## Dependencies

- `mcp[cli]` — FastMCP framework
- `pyyaml` — Configuration loading
- `uvicorn` — ASGI server for streamable-http transport
- `acm-browser` — Local .whl for ACM library catalog extraction (SQLAlchemy + lxml)