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
31 changes: 18 additions & 13 deletions web-console/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# Web console -- cMCP in the browser

The same enforcement the other demos show on the command line, in a small web UI
you can click through in front of an audience. Every number on the page comes
from the real gateway: real tool calls, real Cedar decisions, the real signed
TRACE record, and the real `cmcp verify` output.
you can click through in front of an audience. The scenario is a bank support
agent: it can look up accounts and customers, but policy forbids it from moving
money or exporting records. Every number on the page comes from the real
gateway -- real tool calls, real Cedar decisions, the real signed TRACE record,
and the real `cmcp verify` output.

```
pip install cmcp-runtime
Expand All @@ -13,18 +15,20 @@ python web-console/run.py
`run.py` starts three local processes and opens the console at
`http://localhost:8000`:

- the shared MCP filesystem server on `:9001` (`server/server.py`)
- a mock core-banking MCP server on `:9001` (`tools_server.py`)
- the cMCP gateway on `:8443` (`CMCP_DEV_MODE=1`, software-only TEE)
- this demo's web server on `:8000`

Press Ctrl+C in the terminal to stop all three.

## What to click

1. **`write_file`** and **`read_file`** -- Cedar permits them. The call is
forwarded to the tool and you see the real result.
2. **`list_dir`** -- Cedar forbids it. The gateway returns HTTP 403
(`POLICY_DENY`) and the tool is never contacted.
1. **`get_balance`** and **`get_customer`** -- Cedar permits them. The call is
forwarded to the tool and you see the real record (PII fields come back
masked).
2. **`transfer_funds`** and **`export_records`** -- Cedar forbids them. The
gateway returns HTTP 403 (`POLICY_DENY`) and the tool is never contacted.
The agent can read, but it cannot move money or bulk-export data.
3. **Close session -> TRACE record** -- the gateway signs a `GatewayClaim`:
which tools ran, which were denied, the policy bundle hash, an Ed25519
signature. Saved to `workspace/web-console-claim.json`.
Expand All @@ -42,19 +46,20 @@ one you approved.
The browser never talks to the gateway directly. `webserver.py` serves the UI
and forwards to the gateway server-side, holding the bearer token so it stays
out of the browser and there is no CORS to configure. It is a thin proxy over
the same HTTP endpoints the CLI demos use (`POST /mcp`,
`GET /audit/export`, `POST /sessions/{id}/close`).
the same HTTP endpoints the CLI demos use (`POST /mcp`, `GET /audit/export`,
`POST /sessions/{id}/close`).

The Cedar policy uses the action dialect the runtime evaluates
(`write_file -> Action::"WriteFile"`), the same as `demo-01`. Nothing here is
mocked.
(`get_balance -> Action::"GetBalance"`). Nothing here is mocked except the tool
backend, and even that returns realistic records.

## Files

```
web-console/
run.py launcher (server + gateway + web server)
run.py launcher (tool server + gateway + web server)
webserver.py static UI + /api proxy to the gateway
tools_server.py mock core-banking MCP server (read + write/export tools)
cmcp-config.yaml gateway config (enforcing, software-only)
catalog.json approved tools
policies/ Cedar bundle
Expand Down
118 changes: 85 additions & 33 deletions web-console/catalog.json
Original file line number Diff line number Diff line change
@@ -1,25 +1,58 @@
[
{
"tool_name": "write_file",
"tool_name": "get_balance",
"server": {
"display_name": "Demo Filesystem MCP Server",
"display_name": "Core Banking MCP Server (mock)",
"url": "http://localhost:9001/mcp",
"tls_fingerprint": "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"transport": "http-sse"
},
"approved_definition": {
"description": "Write content to a file in the demo workspace",
"description": "Look up the balance for a bank account",
"input_schema": {
"type": "object",
"required": [
"path",
"content"
"account_id"
],
"properties": {
"path": {
"account_id": {
"type": "string"
},
"content": {
}
}
},
"output_schema": {
"type": "object",
"properties": {
"result": {
"type": "string"
}
}
}
},
"definition_hash": "sha256:7affd4526385567bbce7c86df1b15dd614c0559aaf12213a063e123af9ef8a7d",
"compliance_domain": "internal",
"requires_baa": false,
"sensitivity_level": "confidential",
"added_at": "2026-07-01T00:00:00Z",
"approved_by": "security-team"
},
{
"tool_name": "get_customer",
"server": {
"display_name": "Core Banking MCP Server (mock)",
"url": "http://localhost:9001/mcp",
"tls_fingerprint": "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"transport": "http-sse"
},
"approved_definition": {
"description": "Look up a customer profile (PII fields returned masked)",
"input_schema": {
"type": "object",
"required": [
"customer_id"
],
"properties": {
"customer_id": {
"type": "string"
}
}
Expand All @@ -33,74 +66,93 @@
}
}
},
"compliance_domain": "public",
"definition_hash": "sha256:b424e55dce58f35ff97127904df9a7f837279ede56c604a99988078198fec33e",
"compliance_domain": "pii",
"requires_baa": false,
"sensitivity_level": "public",
"added_at": "2026-06-22T00:00:00Z",
"approved_by": "demo-setup",
"definition_hash": "sha256:7b4738feb4a0c18b2870276996a14d8c2241380c063815cb98b54cb200e7e0d2"
"sensitivity_level": "pii",
"added_at": "2026-07-01T00:00:00Z",
"approved_by": "security-team"
},
{
"tool_name": "read_file",
"tool_name": "transfer_funds",
"server": {
"display_name": "Demo Filesystem MCP Server",
"display_name": "Core Banking MCP Server (mock)",
"url": "http://localhost:9001/mcp",
"tls_fingerprint": "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"transport": "http-sse"
},
"approved_definition": {
"description": "Read a file from the demo workspace",
"description": "Move money between accounts",
"input_schema": {
"type": "object",
"required": [
"path"
"from_account",
"to_account",
"amount"
],
"properties": {
"path": {
"from_account": {
"type": "string"
},
"to_account": {
"type": "string"
},
"amount": {
"type": "number"
}
}
},
"output_schema": {
"type": "object",
"properties": {
"content": {
"result": {
"type": "string"
}
}
}
},
"compliance_domain": "public",
"definition_hash": "sha256:b594a0959a0fe472d219844ff47812a174014d12ae8dd1cd078c8e6446311d48",
"compliance_domain": "pci_data",
"requires_baa": false,
"sensitivity_level": "public",
"added_at": "2026-06-22T00:00:00Z",
"approved_by": "demo-setup",
"definition_hash": "sha256:3bdb3d438df2208aa5b293e0d71260f808365d6fcef4a9cf44702ebed1acc6cc"
"sensitivity_level": "confidential",
"added_at": "2026-07-01T00:00:00Z",
"approved_by": "security-team"
},
{
"tool_name": "list_dir",
"tool_name": "export_records",
"server": {
"display_name": "Demo Filesystem MCP Server",
"display_name": "Core Banking MCP Server (mock)",
"url": "http://localhost:9001/mcp",
"tls_fingerprint": "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"transport": "http-sse"
},
"approved_definition": {
"description": "List files in the demo workspace",
"description": "Bulk-export records from a dataset",
"input_schema": {
"type": "object",
"required": [
"dataset"
],
"properties": {
"dataset": {
"type": "string"
}
}
},
"output_schema": {
"type": "object",
"properties": {
"path": {
"result": {
"type": "string"
}
}
}
},
"compliance_domain": "public",
"definition_hash": "sha256:4e09969b2f6543bcd2cb89a31ff2d05ec80c2f4323b0ff00bf666c307532ca10",
"compliance_domain": "pii",
"requires_baa": false,
"sensitivity_level": "public",
"added_at": "2026-06-22T00:00:00Z",
"approved_by": "demo-setup",
"definition_hash": "sha256:215e8f06120f876cb19bb766a54042a31d521b54cf731f6893c975e4e0861e78"
"sensitivity_level": "confidential",
"added_at": "2026-07-01T00:00:00Z",
"approved_by": "security-team"
}
]
21 changes: 0 additions & 21 deletions web-console/policies/allow-filesystem-tools.cedar

This file was deleted.

22 changes: 22 additions & 0 deletions web-console/policies/support-agent.cedar
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// cMCP Cedar policy -- bank support agent
//
// The runtime maps each tool name to a Cedar action (PascalCase):
// get_balance -> Action::"GetBalance"
// get_customer -> Action::"GetCustomer"
// transfer_funds -> Action::"TransferFunds"
// export_records -> Action::"ExportRecords"
//
// The agent may read account and customer data.
permit (
principal,
action in [Action::"GetBalance", Action::"GetCustomer"],
resource
);

// It may not move money or bulk-export records. forbid overrides permit,
// so these are denied at the gateway (HTTP 403) and never reach the tool.
forbid (
principal,
action in [Action::"TransferFunds", Action::"ExportRecords"],
resource
);
4 changes: 2 additions & 2 deletions web-console/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ def main():
cmcp_log = open(HERE / "cmcp.log", "w")
procs = []
try:
print("-- MCP filesystem server on :9001", flush=True)
print("-- Core banking MCP server on :9001", flush=True)
procs.append(subprocess.Popen(
[sys.executable, str(REPO_ROOT / "server" / "server.py")],
[sys.executable, str(HERE / "tools_server.py")],
stdout=server_log, stderr=server_log))
time.sleep(1)

Expand Down
86 changes: 86 additions & 0 deletions web-console/tools_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Mock business tools for the cMCP browser console.

Plain HTTP JSON-RPC 2.0, same shape the CLI demos' filesystem server uses. These
stand in for a bank's internal MCP tools: two read tools the support agent is
allowed to use, and two write/export tools policy forbids (so they never reach
this server -- they are here only so the "allowed" path has a real upstream).

python tools_server.py # serves on :9001
"""
import json
import pathlib

from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Route

# Canned records, keyed the way a real lookup would be.
ACCOUNTS = {
"AC-4821": {"account": "AC-4821", "type": "Checking", "balance": "$4,182.55",
"currency": "USD", "status": "active"},
"AC-7730": {"account": "AC-7730", "type": "Savings", "balance": "$28,940.10",
"currency": "USD", "status": "active"},
}
CUSTOMERS = {
"C-10293": {"customer_id": "C-10293", "name": "Jordan Rivera", "tier": "Premier",
"email": "j****@example.com", "ssn": "***-**-4821", "kyc": "verified"},
"C-55817": {"customer_id": "C-55817", "name": "Priya Nair", "tier": "Standard",
"email": "p****@example.com", "ssn": "***-**-9037", "kyc": "verified"},
}


def _ok(id_, obj):
text = json.dumps(obj, indent=2) if not isinstance(obj, str) else obj
return {"jsonrpc": "2.0", "id": id_, "result": {"content": [{"type": "text", "text": text}]}}


def _err(id_, code, msg):
return {"jsonrpc": "2.0", "id": id_, "error": {"code": code, "message": msg}}


async def handle(request: Request) -> JSONResponse:
if request.method == "GET":
return JSONResponse({"status": "ok"})
try:
body = await request.json()
except Exception:
return JSONResponse(_err(None, -32700, "parse error"), status_code=400)

id_ = body.get("id")
params = body.get("params", {})
name = params.get("name", "")
args = params.get("arguments", {})

if body.get("method") != "tools/call":
return JSONResponse(_err(id_, -32601, "method not found"), status_code=404)

if name == "get_balance":
rec = ACCOUNTS.get(args.get("account_id", ""))
if not rec:
return JSONResponse(_ok(id_, {"error": "account not found"}))
return JSONResponse(_ok(id_, rec))
if name == "get_customer":
rec = CUSTOMERS.get(args.get("customer_id", ""))
if not rec:
return JSONResponse(_ok(id_, {"error": "customer not found"}))
return JSONResponse(_ok(id_, rec))
# These are policy-forbidden, so the gateway blocks them before they arrive.
# Implemented anyway so the demo is honest about what the tool would do.
if name == "transfer_funds":
return JSONResponse(_ok(id_, {"status": "transferred", "from": args.get("from_account"),
"to": args.get("to_account"), "amount": args.get("amount")}))
if name == "export_records":
return JSONResponse(_ok(id_, {"status": "exported", "dataset": args.get("dataset"),
"rows": 128412}))
return JSONResponse(_err(id_, -32601, f"unknown tool: {name}"), status_code=404)


app = Starlette(routes=[
Route("/mcp", handle, methods=["GET", "POST"]),
Route("/health", handle, methods=["GET"]),
])

if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="localhost", port=9001)
Loading