Skip to content

Architecture overview

┌────────────────────────────────────────────────────────────────────────┐
│                            MCP client (LLM)                             │
└──────────────────────────┬──────────────────────────────────────────────┘
                           │  MCP JSON-RPC over stdio / SSE / HTTP
┌──────────────────────────▼──────────────────────────────────────────────┐
│                            FastMCP server                               │
│                                                                          │
│  ┌────────────┐   ┌──────────┐   ┌──────────┐   ┌─────────────────┐   │
│  │ config.py  │   │ loader.py│   │ tools.py │   │  dispatcher.py  │   │
│  │            │   │          │   │          │   │                 │   │
│  │ YAML + env │ ─►│  Read    │ ─►│ Register │ ─►│  httpx + auth   │   │
│  │ interpol.  │   │  spec,   │   │ one tool │   │  + retry        │   │
│  └────────────┘   │  group,  │   │ per tag  │   └────────┬────────┘   │
│                   │  filter, │   │ group    │            │            │
│                   │  index   │   └──────────┘   ┌────────▼────────┐   │
│                   └──────────┘                  │     auth.py     │   │
│                                                 │ JWT or session  │   │
│                                                 └────────┬────────┘   │
└──────────────────────────────────────────────────────────┬───────────┘
                                                            │  HTTPS
                                                ┌───────────────────────┐
                                                │   Cisco vManage       │
                                                └───────────────────────┘

Modules

Module Responsibility
config.py Load sdwan-mcp.yaml, interpolate ${VAR}, validate.
loader.py Read OpenAPI spec files, adaptively split operations into tools (section → sub-tag → URL path), filter by RO/RW, derive stable action_name per op, build a flat action_name → op index.
fetcher/ Live spec ingestion for vManage 20.16+. Pulls per-operation and per-schema JSON fragments from developer.cisco.com, stitches into a single OpenAPI 3.1 document, validates, and writes specs/<version>/vmanageapi_<flat>.yaml. Drives both the startup auto-fetch and the sdwan-mcp fetch subcommand.
auth.py Two login flows to vManage (JWT and legacy session), header injection, proactive refresh.
transport_auth.py Bearer-token middleware for the SSE / streamable-HTTP transports — constant-time comparison, rate-limited rejection logs, RFC 6750 WWW-Authenticate on 401.
dispatcher.py One httpx.AsyncClient per server. Looks ops up by action_name, routes params (path / query / body), executes, applies the retry + timeout policy, and retries once on session expiry.
pagination.py Scroll- and offset-style auto-follow for bulk endpoints. Stitches up to max_pages pages and surfaces a resumable cursor under pagination.next_cursor.
tools.py Register one FastMCP tool per group, with (action, params) signature.
diff.py Compare two spec versions — added / removed / changed operations. Uses Cisco's operationId (preserved on OperationSpec as a back-reference) so the diff matches Cisco's own identifiers.
server.py CLI parsing, subcommand dispatch (fetch, list-versions), async pre-flight, lifecycle.

Why dynamic instead of generated?

Cisco ships a fresh spec every minor release. With 2,000+ operations, hand-generating Python wrappers means a lot of code churn and a lot of subtle drift. Loading the spec at startup means:

  • A new vManage version is one folder + one config line.
  • The MCP tool descriptions are always the spec the server is actually talking to.
  • The diff utility tells you what changed at a glance.