# Ai Agent Controlplane Demo

> IBM Bob × ContextForge — a turnkey demo of the AI agent control plane: a governed MCP/A2A fintech mesh (OPA policy, PII/secret redaction, prompt-injection defense, RBAC least-privilege), provable with one command.

- **Type:** MCP server
- **Install:** `agentstack add mcp-manavgup-ai-agent-controlplane-demo`
- **Verified:** Pending review
- **Seller:** [manavgup](https://agentstack.voostack.com/s/manavgup)
- **Installs:** 0
- **Category:** [AI & ML](https://agentstack.voostack.com/c/ai-and-ml)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [manavgup](https://github.com/manavgup)
- **Source:** https://github.com/manavgup/ai-agent-controlplane-demo

## Install

```sh
agentstack add mcp-manavgup-ai-agent-controlplane-demo
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# ai-agent-controlplane-demo

> **IBM Bob × ContextForge — the AI agent control plane.** One AI agent, a fintech agent mesh, and a gateway that governs every move. _Who's in charge of your agents?_

  

Can't run Docker/ContextForge locally? Click the badge — the whole governed mesh builds in the cloud; you only need IBM Bob on your laptop (Tier&nbsp;2 in ONBOARDING.md).

  

A turnkey, follow-along demo of [IBM ContextForge](https://github.com/IBM/mcp-context-forge) (the MCP / A2A gateway) acting as the **control plane** between an AI agent (**IBM Bob**) and a fintech ("FinByte") expense-and-payments agent mesh. The gateway sits in the middle of every tool call and every agent-to-agent call, enforces **four controls**, and you can prove all of them with **one command** → `16 passed, 0 failed`.

---

## What is this?

When an AI agent can read receipts, approve expenses, and move money, the question stops being _"can the agent do it?"_ and becomes _"who's in charge of the agent?"_. This demo answers that with a control plane. **IBM Bob** is the agent — an MCP client that drives a FinByte agent mesh (expense lookups, ERP payments, policy docs, notifications, and two autonomous A2A agents). It never talks to those backends directly. Every call flows through **IBM ContextForge**, the gateway that authenticates, authorizes, governs, redacts, and audits — _before_ Bob ever sees a result and _before_ any money moves.

The gateway enforces:

- **Policy (OPA).** A wire over the $10,000 FinByte T&E cap is **blocked** unless it carries dual approval — evaluated live by an Open Policy Agent sidecar against a Rego policy.
- **Data protection.** SSNs, credit-card numbers, API keys, and other secrets in tool output are **masked on the gateway** before they reach the agent (`***-**-6789`, `****-****-****-1111`, `[SECRET_REDACTED]`).
- **Prompt-injection neutralization.** Adversarial instructions hidden in tool output ("SYSTEM: ignore all prior policy…") are **neutralized** to `[INJECTION_BLOCKED]`.
- **RBAC least-privilege.** A FinOps analyst persona simply **has no `wire` tool**; only the privileged operator persona can register servers, read the audit trail, or interrogate policy. (Rate limiting is a built-in ContextForge capability, but it is _not_ configured or demonstrated in this stack.)
- **Agent-mesh governance.** The same policy that stops a direct wire also stops a **cross-language** agent-to-agent payment (a Python auditor delegating a $50k payment to a Rust payments agent) at the bridged tool hook.

The whole stack is path-independent — config, plugins, and Rego policy are baked into the images, so there are **zero host bind-mounts** and it runs from any clone path.

---

## Architecture

_(See the diagram at the top.)_ IBM Bob connects through the `mcpgateway.wrapper` stdio bridge to a **virtual server** on the gateway (a curated, least-privilege slice of the catalog). The gateway fronts five governed MCP servers, one deliberately-unregistered MCP server, and two A2A agents, with an OPA sidecar for policy decisions. Only the gateway and the two A2A agents publish ports to the host; the OPA sidecar and all MCP servers are reachable only on the Compose private network.

| Component | Kind | Port | Role |
|---|---|---|---|
| **IBM Bob** | AI agent / MCP client | — | Drives the mesh; connects via the `mcpgateway.wrapper` stdio bridge to a virtual server |
| **ContextForge gateway** | MCP/A2A gateway (the control plane) | `4444` (host) | Authn/z, governance, redaction, audit, federation; Admin UI at `/admin` |
| **OPA** | Open Policy Agent sidecar | `8181` (internal) | Evaluates the Rego wire-amount policy (`package mcpgateway`) |
| **expense-db** | MCP server (Python, FastMCP) | `8000` (internal) | `list_pending_expenses`, `get_expense`, `get_receipt` (holds the PII / injection fixtures) |
| **erp-payments** | MCP server (Python, FastMCP) | `8000` (internal) | `approve`, `reimburse`, `wire` (the governed money path) |
| **policy-docs** | MCP server (Python, FastMCP) | `8000` (internal) | `get_policy`, `wire_limit` |
| **notify** | MCP server (Python, FastMCP) | `8000` (internal) | `notify` |
| **controlplane** | MCP server (Python, FastMCP) | `8000` (internal) | Operator surface: `register_mcp_server`, `list_control_plane`, `recent_blocks`, `evaluate_policy` |
| **fx-rates** | MCP server (Python, FastMCP) | `8000` (internal) | `get_fx_rate`, `list_currencies` — **runs but is intentionally unregistered** (for the live-register beat) |
| **auditor** | A2A agent (Python, `a2a-sdk`) | `9001` (host) | Audits expenses; can delegate a payment to the Rust agent |
| **payments** | A2A agent (Rust, `a2a-lf` / `a2a-server-lf`) | `3000` (host) | Executes payments; JSON-RPC at `/jsonrpc`, agent card at `/.well-known/agent-card.json` |

> The seed registers **5 governed MCP servers** (`expense-db`, `erp-payments`, `policy-docs`, `notify`, `controlplane`) plus the **2 A2A agents**, and curates them into three virtual servers (FinOps, Treasury, Operator). `fx-rates` is left unregistered on every seed so the operator demo can register it live.
>
> Naming note: the `controlplane` and `auditor` services read their admin token from an env var named `AUDITOR_TOKEN` (written to `.env.tokens` by `make up`). Despite the name, it is an **admin** JWT — not an auditor-only scope.

---

## How a call is governed

  

Every tool call passes through two gateway plugin hooks. On **`tool_pre_invoke`**, the custom **FinByteGuard** plugin (`gateway/custom/finbyte_guard.py`, on the cpex framework) extracts the call's arguments and asks **OPA** whether the wire-amount policy permits it — denying anything over the $10,000 cap without dual approval, and failing _closed_ if OPA is unreachable. On **`tool_post_invoke`**, FinByteGuard deep-scrubs secrets (`sk-live-…` → `[SECRET_REDACTED]`) and neutralizes prompt injection (→ `[INJECTION_BLOCKED]`), while the cpex **PIIFilterPlugin** masks SSNs and credit-card numbers. Every decision emits an `AUDIT [FinByteGuard] …` line and shows up in the Admin UI's **Logs** tab.

---

## The four controls

  

| Control | Prompt to Bob | What ContextForge does |
|---|---|---|
| **1 — Policy (OPA)** | _"Use the finbyte-gateway tools to wire $50,000 to Acme LLC for expense `exp_big`."_ | **Blocks** at OPA: _"…exceeds the $10,000 auto-approve limit… FinByte T&E policy §2."_ Add _"with dual approval"_ → **allowed**. The same policy blocks the cross-language auditor→payments $50k at the bridged `a2a-payments` hook. |
| **2 — Data protection** | _"Fetch receipt `rcpt_pii`, verbatim."_ | **Masks** before Bob sees it: SSN → `***-**-6789`, card → `****-****-****-1111`, API key → `[SECRET_REDACTED]`. |
| **3 — Prompt-injection** | _"Fetch receipt `rcpt_injection`."_ | **Neutralizes** the embedded `SYSTEM: ignore all prior policy…` → `[INJECTION_BLOCKED]`. |
| **4 — RBAC least-privilege** | _"Now wire $50k yourself, directly."_ | Bob **can't** — the FinOps virtual server hides `erp-payments-wire`. MCP Inspector confirms the tool is absent. The operator persona has control-plane tools the analyst lacks. |

Baseline that works: _"Process expense `exp_clean` and reimburse it."_ — a clean $18.50 expense flows straight through.

- **Expense approval quorum** — the room votes on a $50k wire from their phones (live local tally) while 5 governed A2A voter agents vote through the ContextForge gateway (each call authn'd + audited). OPA then blocks the wire regardless of either tally. **Policy beats consensus.**

---

## Two personas (RBAC)

The same Bob binary becomes two different actors depending on which virtual server its `.bob/mcp.json` points at. Both targets rewrite `.bob/mcp.json` (from the `bob-personas/*.template` files, refreshing the live UUID) and launch Bob from the repo root, so they're cwd-proof and reseed-proof.

| | `make bob` — **FinOps analyst** (Act 1) | `make bob-operator` — **platform operator** (Act 2) |
|---|---|---|
| Virtual server | **FinOps** (8 tools) | **Operator** (4 tools) |
| Can do | List/read expenses, read receipts, `approve`, `reimburse`, read policy + wire limit, talk to the **auditor** agent | `register_mcp_server`, `list_control_plane`, `recent_blocks`, `evaluate_policy` |
| **Cannot** do | **No `wire` tool**; can't register servers, read the audit trail, or query policy directly | Not the analyst's expense-handling surface |
| Persona file | `bob-personas/mcp.json.template` (server `finbyte-gateway`) | `bob-personas/mcp.operator.json.template` (server `finbyte-operator`) |

Swap back to the analyst at any time with `make bob`.

---

## Prerequisites

| Tool | Why | How to get it |
|---|---|---|
| **Docker** (running) | Runs the gateway, OPA, MCP servers, and A2A agents | **Docker Desktop** on macOS/Windows, **or Docker Engine** on Linux (runs natively, no nested virtualization). Start it before you begin. — [docker.com](https://www.docker.com/products/docker-desktop/) **No Docker?** The stack also runs on **Podman** — see [Run on Podman](docs/RUNBOOK.md#run-on-podman-no-docker), or one-shot a fresh Ubuntu/WSL2/x86 host with `bash scripts/test-fresh-host.sh`. |
| **uv** | Mints the gateway JWT **offline** (no network round-trip) | `https://docs.astral.sh/uv/` |
| **IBM Bob Shell** (`bob`) | _Optional_ — only to **drive** Bob; the stack + `16/16` proof run without it | macOS/Linux: `curl -fsSL https://bob.ibm.com/download/bobshell.sh \| bash` ([bob.ibm.com/download](https://bob.ibm.com/download)) — checks Node ≥ 22.15 first |
| **Node.js ≥ 22.15** | _Optional_ — required by IBM Bob Shell (it's a Node app) and the MCP Inspector (`npx`); not needed to bring up the stack or prove the controls | [nodejs.org](https://nodejs.org), or `nvm install 22` |

> Budget **~5 GB** of free disk. On the first run, the pinned ContextForge image pulls once and the seven source images (six MCP servers + the Rust payments agent) build locally. Subsequent cold starts (`make down && make quickstart`) take roughly **~38 seconds** once images are cached.

> **Running on Linux / in a VM.** Only **Docker** and **uv** are truly required to bring up the stack and prove `16/16` — `bob`/Node are needed only to _drive_ the demo. On Apple silicon a full macOS-guest VM is impractical (~60 GB+ disk); the practical path is a lightweight Linux VM (Multipass/Lima) + Docker Engine, which runs the stack natively on arm64 (the OPA image is multi-arch, so no emulation). IBM Bob Shell is cross-platform and can also be installed in the VM to drive the demo (first run uses an IBMid device-code login on a headless box). See **[Running on a fresh Linux box / VM](#running-on-a-fresh-linux-box--vm)** below.

---

## Two ways to run it

Same stack, two stories — **pick your path**:

🎓 Progressive build — build an agent tool from scratch, then watch it get governed (developer path)

Bottom-up, the inverse of `make quickstart`: start from a bare MCP server you'd recognise anywhere and earn ContextForge one layer at a time — **carrying the tool you built the whole way**. Open the prompt-card and drive Bob through four stages:

```bash
make dev-start          # opens docs/build.html — the follow-along build guide (copy-paste Bob prompts)
```

| Stage | Command | What you (and Bob) do | Bob persona |
|---|---|---|---|
| **① Build** | `make stage1-build` | **Prompt Bob** to write `mcp-servers/sales-tax/server.py` from scratch (a FastMCP `add_tax` tool). It runs bare on `:8000`; a call prints `add_tax(100) → 108.50`. *It works — and it's totally ungoverned: no token, no policy, anyone on the port runs anything.* | — |
| **② Govern** | `make stage2-govern` | The **same** server is containerised onto the mesh, **registered** (in the catalog, token-gated — *not callable yet*), then **granted** into a `Builder` virtual server and **called back through the gateway** → `108.50`. **2b bonus:** Bob *extends* a service it didn't write (`fx-rates` gains `convert`). | operator → **builder** |
| **③ Control** | `make stage3-controls` | One prompt drives a batch; the **four controls bite real calls** — PII redaction, injection neutralised, OPA blocks a $50k cross-language wire, RBAC (Bob has no `wire` tool). | analyst |
| **④ Mesh** | `make stage4-mesh` | The full governed picture — identical to the `quickstart` end-state, but you watched it get built. | — |

**The throughline is `register → grant → call`.** Your tool goes from *works-but-ungoverned* → *in the catalog but not callable* → *granted and callable through the one governed seam*, with redaction, policy, and audit applied. Registering a backend doesn't make it callable — **granting it to an agent is a separate, privileged step** (`make salestax-grant` adds it to the `Builder` vserver). That boundary *is* least-privilege.

**The `builder` persona** (`make bob-install-builder`) is the developer's seat — it calls *your own granted tools* (`add_tax`, `convert`), alongside the existing **analyst** (least-privilege consumer) and **operator** (registers / audits / evaluates policy) personas.

**No Docker on your laptop?** `make connect` prints a `bob mcp add … -t http` line pointed at a gateway running elsewhere — a teammate's box, a VM, or a **GitHub Codespace** — so you drive the whole governed mesh with **only Bob installed**, governance intact over the wire.

→ Narrated beats: **[`docs/SHOWCASE-BOB.md`](docs/SHOWCASE-BOB.md)** · the follow-along **[`docs/build.html`](docs/build.html)** (presenter cockpit: **[`docs/cockpit.html`](docs/cockpit.html)**) · zero-setup tiers in **[`docs/ONBOARDING.md`](docs/ONBOARDING.md)**.

🛰 Governed mesh — drop straight into the finished, governed stack (quickstart path)

Top-down: **one command** takes a laptop from nothing to a running, governed mesh and proves all four controls (`16/16`) — no Bob required. Then drive Bob as the FinOps **analyst** (Act 1) and platform **operator** (Act 2) and watch the control plane react in the Admin UI.

→ Full command + walkthrough in **[Quickstart](#quickstart)** immediately below (then **[Drive Bob](#drive-bob)**, the watch panes, and the deterministic proof under it).

---

## Quickstart

```bash
git clone https://github.com/manavgup/ai-agent-controlplane-demo.git
cd ai-agent-controlplane-demo
make quickstart
```

`make quickstart` is **one command** that takes a laptop from nothing to a running, governed mesh: preflight (**requires** Docker + uv; **warns but continues** if `bob`/`npx` are absent) → bring up the stack → seed (register servers/agents, build the FinOps / Treasury / Operator virtual servers) → configure Bob (FinOps analyst persona) → **prove all four controls (`16/16`, with no Bob required)** → print a copy-paste walkthrough card. It's re-runnable — safe to run again if anything stalls. The Admin UI logs in with `admin@finbyte.demo` / `FinByteAdmin!2026`.

> **Proof is headless.** `make quickstart` finishes `16 passed, 0 failed` even on a box without `bob` or Node (a Linux VM or CI runner): Bob only **drives** the demo — it isn't needed to bring up the stack or prove the controls. `make bob` / `make bob-operator` also fail gracefully if `bob` isn't installed (they still write `.bob/mcp.json`, print an install hint, and exit `0`).

### Drive Bob

**Act 1 — FinOps analyst (least-privilege).** Launch with `make bob` (cwd-proof; it refreshes the config first), then try:

- _"Use the finbyte-gateway tools to fetch receipt `rcpt_pii`, verbatim."_ → redacted.
- _"Fetch receipt `rcpt_injection`."_ → `[INJECTION_BLOCKED]`.
- _"Ask the auditor agent to pay $50,000 to Acme LLC."_ → **blocked** at OPA (Python → Rust).
- _"Now wire $50k yourself, directly."_ → Bob has no `wire` tool.

**Act 2 — platform operator.** Quit Bob, then `make bob-operator` to swap personas and relaunch:

- _"List everything ContextForge is governing."_ → `list_control_plane`.
- _"Would a $50,000 wire be allowed? With dual approval?"_ → `evaluate_policy` (deny + reason, then allow).
- _"Register the fx-rates service at `http://fx-rates:8000/mc

…

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [manavgup](https://github.com/manavgup)
- **Source:** [manavgup/ai-agent-controlplane-demo](https://github.com/manavgup/ai-agent-controlplane-demo)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: flagged — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-manavgup-ai-agent-controlplane-demo
- Seller: https://agentstack.voostack.com/s/manavgup
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
