# 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`), bot logic walker, and snapshot store are
implemented and unit-tested (647 tests). Writes are opt-in and gated. **Not yet validated
against a live Control Room instance** — the A360 filter operator set and bot JSON node
schema are permissive parsers, awaiting field verification.

---

## 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 |

### 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` | same as `core` | Audit/log reads (deeper reads land later) |
| `diagnose` | core + `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).

### 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.

---

## 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.

## 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).
