Skip to main content
Glama
jshsakura

a360-mcp

by jshsakura
README.md
# a360-mcp

[![PyPI version](https://img.shields.io/pypi/v/a360-mcp.svg)](https://pypi.org/project/a360-mcp/)
[![Python Version](https://img.shields.io/pypi/pyversions/a360-mcp)](https://pypi.org/project/a360-mcp/)
[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)

MCP server for **Automation Anywhere Automation 360 (A360) Control Room** — bot logic
analysis and execution failure diagnostics.

> Community project. Not affiliated with or endorsed by Automation Anywhere, Inc.
> "Automation Anywhere" and "Automation 360" are trademarks of their respective owner.

---

## What it is for

A bot ran. It died. **Which action, why, and what was missing upstream?**

This server pulls the failed execution from the Control Room, pulls the bot's logic, and
maps one onto the other — so the answer is *"action 37, `Open Excel`, file not found; there
is no existence check before it and no error handler around it"* rather than a status code.

## What it is not

Automation Anywhere ships **MCP Inbound** (`https://<control-room>/mcp`) from v.38, which
exposes bots as tools so an assistant can **run** them. If running bots is what you need,
use the vendor feature — it has RBAC, governance logging, and per-automation registration.

It does not read execution history, bot logic, WLM queues, or audit logs. That is this
project's scope. The two are complementary.

## Status

Beta. Diagnostic pipeline (`diagnose_execution`), audit correlation (`correlate_failure`),
bot logic walker, and snapshot store are implemented and unit-tested (819 tests, 93%
coverage). Writes are opt-in and gated. **Not yet validated against a live Control Room
instance** — the A360 filter operator set, the audit field names, and the bot JSON node
schema are permissive parsers, awaiting field verification. Everything unconfirmed is
marked `[UNVERIFIED]` at its definition.

---

## Requirements

- Python 3.11+
- An Automation 360 Control Room account with an **API key**
  (needs a custom role carrying `Generate API-Key` — no system role has it by default)
- Read privileges on the bots you want to diagnose (`View my bots`)

## Setup

The default is **`uvx`** — no separate install step, it just runs.

```bash
# Install uv (one-time)
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows PowerShell
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

# Verify the server runs
uvx --from a360-mcp a360-mcp --version
```

**To update** — uvx caches the last version; pull new releases with `--refresh`:

```bash
uvx --refresh --from a360-mcp a360-mcp --version
```

### If uvx is blocked — `pip`

Windows Smart App Control blocks `uvx` (it unpacks an unsigned temporary executable on
every run). Use pip instead:

```powershell
pip install a360-mcp
python -m a360_mcp --version
```

Launch with `python -m a360_mcp` rather than the `a360-mcp` console script — that script is
an unsigned `.exe` shim pip generates, and SAC blocks it too.

---

## MCP client configuration

Add the server to your client's config file. **The `env` block is identical no matter how
you installed** — only `command` / `args` differ.

| Install | `command` | `args` |
|---|---|---|
| uvx (default) | `uvx` | `["--from","a360-mcp","a360-mcp"]` |
| pip | `python` | `["-m","a360_mcp"]` |

Required env vars:

| Variable | Meaning |
|---|---|
| `A360_CONTROL_ROOM_URL` | Control Room base URL (e.g. `https://cr.example.com`) |
| `A360_USERNAME` | Service account username |
| `A360_API_KEY` **or** `A360_PASSWORD` | One of the two — mutually exclusive |

Optional env vars (all default to the safe value — set them only to change it):

| Variable | Default | Meaning |
|---|---|---|
| `A360_ALLOW_WRITES` | `0` | Opt in to mutating tools. See [Safety](#safety) before setting it. |
| `A360_WRITE_GUARDS_FAIL` | `open` | Set to `closed` so a guard that cannot decide blocks instead of allowing. |
| `A360_TOOL_PACKAGE` | `core` | Which tool layer to expose: `core`, `bot_read`, `log_read`, `diagnose`, `full`. |
| `A360_REQUEST_TIMEOUT` | `60` | Per-request timeout, seconds. |

Accepted truthy values are `1`, `true`, `yes`, `on` (case-insensitive); anything else — including
an unset or empty variable — reads as off. Every one of these also has a CLI flag
(`--allow-writes`, `--tool-package`, …), and the flag wins over the environment.

### Antigravity

`.agents/mcp_config.json` (project root) or `~/.gemini/config/mcp_config.json` (global):

```json
{
  "mcpServers": {
    "a360": {
      "command": "uvx",
      "args": ["--from", "a360-mcp", "a360-mcp"],
      "env": {
        "A360_CONTROL_ROOM_URL": "https://cr.example.com",
        "A360_USERNAME": "svc_mcp",
        "A360_API_KEY": "${A360_API_KEY}"
      }
    }
  }
}
```

### Claude Code

`.mcp.json` (project root) or `~/.claude.json` (global):

```json
{
  "mcpServers": {
    "a360": {
      "command": "uvx",
      "args": ["--from", "a360-mcp", "a360-mcp"],
      "env": {
        "A360_CONTROL_ROOM_URL": "https://cr.example.com",
        "A360_USERNAME": "svc_mcp",
        "A360_API_KEY": "${A360_API_KEY}"
      }
    }
  }
}
```

### Codex

`.codex/config.toml` (project) or `~/.codex/config.toml` (global):

```toml
[mcp_servers.a360]
command = "uvx"
args = ["--from", "a360-mcp", "a360-mcp"]
# pip: command = "python"  /  args = ["-m", "a360_mcp"]

[mcp_servers.a360.env]
A360_CONTROL_ROOM_URL = "https://cr.example.com"
A360_USERNAME = "svc_mcp"
A360_API_KEY = "${A360_API_KEY}"
```

### OpenCode

`opencode.json` (project root):

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "a360": {
      "type": "local",
      "command": ["uvx", "--from", "a360-mcp", "a360-mcp"],
      "enabled": true,
      "environment": {
        "A360_CONTROL_ROOM_URL": "https://cr.example.com",
        "A360_USERNAME": "svc_mcp",
        "A360_API_KEY": "${A360_API_KEY}"
      }
    }
  }
}
```

### `${ENV}` reference

The CLI expands `${VAR}` as a **full value only** — `A360_API_KEY=${A360_API_KEY}` resolves
to the value of the `A360_API_KEY` env var at startup. Partial references like
`${ENV}_suffix` are rejected to prevent silent misconfiguration.

---

## Tools

The server exposes a layered tool set, gated by `A360_TOOL_PACKAGE` (default `core`):

| Package | Includes | Use case |
|---|---|---|
| `core` | `get_execution`, `list_executions`, `get_bot_actions` | Read-only triage |
| `bot_read` | same as `core` | Bot content reads (deeper reads land later) |
| `log_read` | core + `list_audit_events`, `correlate_failure` | "What changed?" investigations |
| `diagnose` | log_read + `diagnose_execution` | Default for most workflows |
| `browser_read` | diagnose + inspect_console / inspect_network / screenshot_debug / get_computed_style | Read-only browser diagnostics (Playwright optional) |
| `browser` | browser_read + open_debug_window / close_debug_window | Full browser layer (consumes a session slot) |
| `full` | everything | No gating |

Set with `A360_TOOL_PACKAGE=diagnose` (env) or `--tool-package diagnose` (CLI).

Tool schemas are re-sent on every request, so each tier's cost is a standing tax. Measure it
with `python scripts/measure_tool_tokens.py` (add `--per-tool` for the breakdown):

| Package | Tools | Tokens/request |
|---|---:|---:|
| `core` | 3 | 250 |
| `log_read` | 5 | 518 |
| `diagnose` | 6 | 609 |
| `full` | 12 | 1,013 |

### Headline tool: `diagnose_execution`

Given an execution ID, returns a verdict-first diagnosis:

1. Fetch the execution (`GET /v3/activity/execution/{id}`).
2. If the status is not failure-family, return a one-line verdict immediately.
3. Feature-detect the `error` key, fall back to parsing `message`.
4. Fetch the bot logic (`GET /v2/repository/files/{fileId}/content`).
5. Walk action nodes (iterative DFS, no recursion).
6. Map the failure onto a node by line number.
7. Pull upstream / downstream context and Try/Catch presence.
8. If no Try/Catch, emit an **instrumentation prescription** ("place `Catch` with line
   number + exception message assignments, log to server, screen capture inside").
9. Snapshot the diagnosis locally (90-day CR retention fallback).

Bot JSON never enters the model context — only the parsed action surface and a disk path.

### The other half: `correlate_failure`

`diagnose_execution` says **which action died**. It cannot say **why now** — a bot that ran
green for months and broke this morning did not rewrite its own logic. Something around it
changed, and the audit log is the only API surface that records it.

`correlate_failure(execution_id)` bounds the audit log to a window around the failed run
(24h before by default, through 5 minutes after it ended), keeps the mutations, and ranks
them by how closely they relate to the run:

- an event naming the bot or its `fileId` outranks everything else
- among those, the closest in time wins (linear decay across the lookback window)
- events *after* the run ended are never suspects — a change made afterwards cannot have
  broken it, and listing it is how a reader ends up chasing a rollback
- an event whose timestamp will not parse is kept, but sorted last: dropping it would hide
  a change, ranking it would rank it on a guess

Every suspect carries a `why_suspect` string, and **the verdict never claims causation** —
it says "closest change", not "cause". At this layer nothing distinguishes the redeploy that
broke the bot from the redeploy that merely landed the same morning, and a confident wrong
answer is the worst failure mode a diagnostic tool has.

**The Control Room is remote and prunes silently.** A window that predates the 180-day audit
retention comes back as an empty list with HTTP 200 — indistinguishable from a window in
which nothing happened. Past that horizon the response carries a `retention_warning` and the
verdict says *no conclusion can be drawn*, never "no changes are recorded". Absence of
evidence from a remote that deletes on a schedule is not evidence of absence.

`list_audit_events` is the browse counterpart — filter by user, action substring, or time
range. Only the time bound is applied server-side; `user` and `action` are matched in
process, because the audit field names are unverified and stacking three guesses turns a
refinement into a 400 that looks like "no matching events".

Both tools are written for a tight output budget, since tool results are re-read on every
turn: empty fields are dropped per row, lists switch to columnar `{columns, data}` past three
rows (repeated keys are the single largest share of the bytes), and the `limitations` note —
audit covers Control Room actions, not bot-internal logging; `Log text to file` is
`FILE_LOCAL` and unreachable by any API; loop/branch/variable trajectory is exposed by no
endpoint — is attached only when the result is empty and silence could be misread. A result
that already lists what the audit log holds does not need to be told what it doesn't.

---

## Authentication

The server uses `POST /v2/authentication` with the `X-Authorization: <token>` header
(**no `Bearer` prefix**). Tokens live 20 minutes; refreshes are serialized under a mutex
because **refresh kills the previous token**. An account may hold only **5 concurrent
sessions** — the token is cached and the optional debug browser window consumes one slot
too.

API key is preferred for service accounts; `password` is supported but should be paired
with a non-2FA role (the A360 v2 auth request has no `mfaCode` field).

## Safety

- **Writes are opt-in per instance** — `allow_writes` defaults to `False`. A missing flag
  never means "writable".
- **Mutating tools** (deploy / publish / update / delete / create / cancel / stop / upload
  / rename / move / checkout / checkin, plus the browser window tools) require
  `approve=true` in the tool call.
- **Publish-grade tools** (deploy / publish) require an **additional**
  `confirm_environment=prod` field, so a single confirmation cannot ship to production.
- **The browser layer never clicks.** It observes console, network, screenshots, and
  computed CSS. `inspect_*` tools structurally cannot open a window — only
  `open_debug_window` can.
- **Fail-open by default** for write guards; flip with `A360_WRITE_GUARDS_FAIL=closed`.
- **Diagnosed executions are snapshotted locally on first read** — the 90-day cloud
  retention limit has no backup path otherwise.

Every refusal states the next action. Never refuses without a path forward.

### Turning writes on

Writes are a per-instance setting, so you change them where you configured the server — no
code change, no rebuild. Add to the `env` block of any client config above:

```json
"env": {
  "A360_ALLOW_WRITES": "1",
  "A360_WRITE_GUARDS_FAIL": "closed"
}
```

or pass `--allow-writes` on the command line. Two things worth knowing before you do:

- **Pair it with `A360_WRITE_GUARDS_FAIL=closed`.** Guards fail *open* by default, which is
  harmless while the server is read-only and much less so once it is not: a guard that
  cannot reach a determination would let the write through.
- **A blocked write costs zero network calls.** The gate is checked before the request is
  built, so turning writes off can never leave a half-applied change behind.

Leaving both unset keeps the server strictly read-only, which is the intended default —
diagnosis needs no writes, and bot execution is what the vendor's own MCP Inbound is for.

## Cloud retention

| Data | Retention |
|---|---|
| Execution history | 90 days |
| Audit log | 180 days (365 in some regions) |

Diagnosed executions are cached under `state_dir()/snapshots/{account_key}/` with the
bot logic at time of diagnosis. Re-diagnosing an old execution reads from disk, not the
Control Room, and the response is marked `original_expired` past 90 days.

---

## Developer setup

```bash
git clone https://github.com/jshsakura/a360-mcp.git
cd a360-mcp
uv venv --python 3.12 .venv
uv pip install -e ".[dev]"

# Tests — always run pytest under the memory-cap wrapper, raw pytest has
# killed the local tmux session before.
saferun -m 4G .venv/bin/pytest tests/ -q

# Lint / format / type
.venv/bin/ruff check src tests
.venv/bin/black src tests
.venv/bin/isort src tests
.venv/bin/mypy src
```

Browser tests are mocked (Playwright is a soft dependency, lazy-imported). To exercise
the real browser layer:

```bash
uv pip install -e ".[browser]"
playwright install chromium
```

## License

Apache-2.0. See [LICENSE](LICENSE).

TDQS

B3.4/5.0

Scored across 3 tools

Disambiguation5/5

Each tool has a clear, distinct purpose: get_bot_actions retrieves bot action definitions, get_execution fetches a single execution's details, and list_executions searches execution history. There is no ambiguity or overlap between these operations.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern, using get_ for singular resources and list_ for collections. The naming is predictable and adheres to standard conventions.

Tool Count4/5

With only 3 tools, the server is at the lower end of the typical range, but each tool covers a distinct aspect of the domain (bot actions and executions). It feels slightly thin but is still reasonable for a focused utility.

Completeness2/5

The server lacks essential operations such as listing bots (making it impossible to discover bot IDs for get_bot_actions) and does not support execution lifecycle actions like starting or canceling executions. This creates significant gaps for agents trying to perform broader workflows.

Maintenance

ActivitySlowing
ResponsivenessNo issues