Skip to main content
Glama
PalNilsson

PanDA Gateway

by PalNilsson
README.md
# PanDA Gateway

A thin, stateless MCP routing layer for the PanDA ecosystem. The gateway sits
between the PanDA Monitor (or any MCP client) and upstream MCP servers such as
Bamboo MCP and PanDA MCP, exposing a single MCP endpoint (Streamable HTTP) and
routing each `tools/call` to the correct upstream based on a namespace prefix
in the tool name.

The gateway carries no LLM logic, no planning, no synthesis — those remain in
Bamboo MCP.

```
PanDA Monitor (MCP client)
        │  MCP / Streamable HTTP  (Bearer token)
        ▼
┌─────────────────────────────────────────────┐
│              PanDA Gateway                  │
│  GatewayServer · UpstreamRegistry · Router  │
└─────────────────────────────────────────────┘
        │           │           │          │
   Bamboo MCP   PanDA MCP   Rucio MCP   CRIC MCP
   bamboo.*     panda.*     (future)    (future)
```

Developed under DOE REDWOOD WBS 2.4.3 (Bamboo MCP / Agentic PanDA).

Deeper reference docs live in `docs/`:
[architecture.md](docs/architecture.md) (components and design decisions),
[catalog-and-search.md](docs/catalog-and-search.md) (how the tool catalog
and `gateway.search_tools` work),
[security-and-resilience.md](docs/security-and-resilience.md) (trust model,
hardening, and failure recovery — read this before any non-local
deployment), and
[examples.md](docs/examples.md) (example prompts, how to try them, and
common first-run errors).

## Installation

```bash
pip install -e .                    # runtime
pip install -e ".[dev]"             # + tests, linting, type checking
pip install -e ".[observability]"   # + OpenTelemetry tracing
```

Requires Python ≥ 3.11.

## Quick start (minimal: Bamboo MCP only, no tokens)

This is the smallest working setup: one Bamboo MCP upstream, no
authentication anywhere. Use it for local development and first
integration tests.

1. Start your Bamboo MCP server (assumed below at `http://localhost:8000/mcp`).

2. Use the provided `gateway.minimal.toml` (edit the `url` if Bamboo runs
   elsewhere):

   ```toml
   [gateway]
   host = "127.0.0.1"
   port = 8090
   auth_disabled = true     # no inbound token; local development only
   separator = "."

   [rag]
   enabled = true

   [[upstreams]]
   namespace = "bamboo"
   url = "http://localhost:8000/mcp"
   # no bearer_token_env / token_file -> unauthenticated upstream connection
   ```

3. Run the gateway:

   ```bash
   panda-gateway --config gateway.minimal.toml
   # equivalently: python -m panda_gateway --config gateway.minimal.toml
   ```

4. Verify:

   ```bash
   curl http://127.0.0.1:8090/healthz
   # -> {"service": "panda-gateway", "status": "ok", ...,
   #     "upstreams": [{"namespace": "bamboo", "state": "up", "tools": N, ...}]}
   ```

   The MCP endpoint is `http://127.0.0.1:8090/mcp` (Streamable HTTP). Point
   any MCP client at it; Bamboo's tools appear as `bamboo.<tool>`, e.g.
   `bamboo.bamboo_answer`. `scripts/verify_gateway.py` is a small smoke
   test that connects, lists tools, and calls one — run it against this
   setup with:

   ```bash
   python scripts/verify_gateway.py
   # or, against a different address/token:
   python scripts/verify_gateway.py http://127.0.0.1:8090/mcp --token "$PANDA_GATEWAY_TOKEN"
   ```

5. **Try it through an LLM.** `verify_gateway.py` proves the routing works,
   but calling a hardcoded tool name isn't the experience an actual user
   will have. Claude Desktop is a quick way to see the real one — an LLM
   reading the gateway's catalog and picking a tool on its own from a
   plain-English question. Add this to its local MCP server config (this
   runs the gateway as a subprocess Claude Desktop manages directly,
   instead of the standalone server from step 3) and restart it:

   ```json
   {
     "mcpServers": {
       "panda-gateway": {
         "command": "panda-gateway",
         "args": ["--config", "/absolute/path/to/gateway.minimal.toml", "--stdio"]
       }
     }
   }
   ```

   Then just ask it something in a new chat — e.g. "How many jobs failed
   today?" — and watch it pick `bamboo.bamboo_answer` on its own, with no
   tool name typed anywhere. See
   [docs/examples.md](docs/examples.md#claude-desktop-concretely) for the
   production version of this config (environment variables, absolute
   paths, and other gotchas that show up once real upstream auth is
   involved) and more prompts to try.

`auth_disabled = true` logs a prominent warning at startup; never use it
beyond localhost or a trusted network.

## Running in production

```bash
export PANDA_GATEWAY_TOKEN=...   # inbound token (required)
export BAMBOO_TOKEN=...          # per-upstream tokens as configured
panda-gateway --config gateway.toml
```

If `--config` is omitted, the path is read from `PANDA_GATEWAY_CONFIG`.
Clients must then send `Authorization: Bearer $PANDA_GATEWAY_TOKEN`; only
`GET /healthz` stays unauthenticated.

## Deployment modes

* **HTTP (production):** the form above — uvicorn serving Streamable HTTP
  at `/mcp`, Bearer auth required (unless `auth_disabled = true`).
* **stdio (development):** `panda-gateway --config gateway.toml --stdio`
  runs the gateway as a local subprocess instead of a network service,
  speaking MCP directly over stdin/stdout — there's no HTTP layer in this
  mode, so inbound auth (`PANDA_GATEWAY_TOKEN`) doesn't apply at all. This
  is the mode local MCP clients that spawn their own subprocess (e.g.
  Claude Desktop's `mcpServers` config) expect. See
  [docs/examples.md](docs/examples.md#claude-desktop-concretely) for a
  concrete walkthrough, including an environment-variable gotcha that
  comes with subprocess-launched clients.

## Configuration

See `gateway.example.toml` for a complete annotated example. Minimal form:

```toml
[gateway]
host = "0.0.0.0"
port = 8090
bearer_token_env = "PANDA_GATEWAY_TOKEN"
separator = "."          # namespace separator in tool names

[[upstreams]]
namespace = "bamboo"
url = "https://aipanda033.cern.ch:8000/mcp"
bearer_token_env = "BAMBOO_TOKEN"
tls_verify = true
ca_bundle_env = "SSL_CERT_FILE"

[[upstreams]]
namespace = "panda"
url = "https://panda-mcp.cern.ch/mcp"
token_file = "~/.panda_id_token"   # OIDC token, re-read on every reconnect
use_sse = false                    # set true if PanDA MCP serves SSE only
```

Each upstream authenticates with **either** `bearer_token_env` (token from an
environment variable) **or** `token_file` — or neither, for open endpoints.
`token_file` understands the JSON token cache written by `get-panda-token`
(the `id_token` field is used) as well as plain-text token files, and is
re-read on every reconnect so externally renewed tokens apply automatically.
Following Bamboo's `panda_mcp_session.py`, the token is sent as both
`Authorization` and `X-Auth-Token`, and an optional `origin = "<vo>"` is sent
as the `Origin` header.

Note on the separator: the handover convention is `bamboo.*` / `panda.*`, but
some MCP clients validate tool names against `^[a-zA-Z0-9_-]+$` and reject
dots. If the Monitor's client stack does, set `separator = "__"` — routing is
separator-agnostic.

Two more top-level tables exist beyond what's shown above: `[backoff]`
(reconnect tuning) and `[transport_security]` (inbound Host/Origin
validation, off by default — see
[docs/security-and-resilience.md](docs/security-and-resilience.md) before
enabling it or deploying anywhere non-local). Per-upstream, `allow_redirects`
and the `init_timeout`/`ping_timeout`/`probe_timeout` deadlines are also
documented there and in `gateway.example.toml`; the defaults are sensible
for most deployments and rarely need changing.

## Behaviour

* **Routing** is a single dict lookup on the namespace prefix. Unknown
  namespaces return JSON-RPC `-32602`; a configured but unavailable upstream
  returns `-32603` naming the upstream, so operators can see which capability
  is missing.
* **Degraded service is visible**: tools of a down upstream are absent from
  `tools/list`; other namespaces keep working.
* **Health checks** are two-tier: a liveness ping every 45 s and a
  `tools/list` probe every 12 min per upstream (both configurable). An
  upstream `notifications/tools/list_changed` triggers an immediate probe.
  Failures cause reconnection with exponential backoff and jitter.
* **Tool catalog** is served from a probe-refreshed cache, with a ChromaDB
  semantic index exposed via the `gateway.search_tools` tool — see
  [docs/catalog-and-search.md](docs/catalog-and-search.md) for how the
  catalog is built, why the semantic index exists, and how both stay
  consistent with actual upstream availability.
* **`GET /healthz`** (unauthenticated) returns per-upstream status JSON —
  machine-readable groundwork for the Phase 2 dashboard.
* **Resilience**: crash isolation between upstreams, bounded health-check
  deadlines, and immediate reconnection on a router-observed failure — see
  [docs/security-and-resilience.md](docs/security-and-resilience.md) for
  the full detail, including the trust model this gateway is built for.

## Development

```bash
python -m pytest tests/    # 122 tests
flake8 panda_gateway tests
pyright
```

Tests run entirely in-process (fake upstream MCP servers over memory streams,
deterministic embeddings) — no network and no model downloads.

`scripts/verify_gateway.py` is a small end-to-end smoke test against a
*running* gateway (see the quick start above) — not part of the pytest
suite, since it needs a live process and an upstream to talk to.

## Attribution

Session-lifecycle, health-check, retry, and observability patterns are adapted
from IBM ContextForge (`mcp-contextforge-gateway`, Apache-2.0). See `NOTICE`.