Skip to main content
Glama
README.md
# Super-MCP
# OData + OpenAPI MCP Server

Dynamically generates tools from any **OData `$metadata`** or **OpenAPI / Swagger spec** and lets you query them with natural language via **SAP AI Core (GPT-4o)**.

- Load once via `/generate` — tools persist to disk and **auto-load on every restart**
- `spec_id` is always optional — auto-detected when only one spec is loaded
- Credentials live **only in `.env` on the server** — never passed through API requests or seen by the LLM
- Full **RBAC** (read-only default, admin required for writes)

Two ways to use it:

| Mode | Entry point | Best for |
|------|------------|----------|
| **REST API** | `api.py` | curl, Postman, programmatic access; docs at `/docs` |
| **MCP server** | `mcp_server.py` | Claude Desktop, Claude Code, any MCP client |

---

## Project Structure

```
mcp/
├── .env                    # Credentials — SAP AI Core + per-spec API keys (never commit)
├── .env.example            # Template — copy to .env and fill in values
├── requirements.txt
│
├── api.py                  # FastAPI REST server — primary entry point
├── mcp_server.py           # FastMCP server for Claude Desktop / Claude Code
│
├── odata_parser.py         # Parses OData v2/v4 $metadata XML
├── odata_executor.py       # Executes OData HTTP calls (GET/POST/PATCH/DELETE + CSRF)
├── openapi_parser.py       # Parses OpenAPI 3.x and Swagger 2.0 (JSON or YAML)
├── openapi_executor.py     # Executes HTTP calls for OpenAPI operations + 401-retry
├── auth_manager.py         # Reads credentials from env vars; fetches/refreshes tokens
├── rbac_manager.py         # Role-based access control (default = read-only, admin = writes)
├── tool_generator.py       # Writes tools/{spec_id}.py from a parsed spec
├── sap_ai_client.py        # SAP AI Core OAuth2 + GPT-4o client
├── config.py               # Loads SAP AI Core config from .env
├── state.py                # In-memory spec / tool registry (shared by api.py + mcp_server.py)
│
└── tools/                  # Auto-generated tool files (auto-loaded on startup)
    └── .gitkeep
```

---

## Quick Setup

```bash
cd mcp
pip install -r requirements.txt
cp .env .env.example       # keep a credential-free template for the repo

# Start the REST API
python3 api.py
# → http://localhost:8080
# → Swagger UI: http://localhost:8080/docs
```

---

## REST API Endpoints

### OData

| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/api/v1/generate` | Fetch OData `$metadata` URL → parse → save `tools/{id}.py` |
| `POST` | `/api/v1/query` | Natural-language query against a loaded OData spec |

### OpenAPI / Swagger

| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/api/v1/generate/openapi` | Fetch OpenAPI/Swagger spec → parse → save `tools/{id}.py` |
| `POST` | `/api/v1/query/openapi` | Natural-language query against a loaded OpenAPI spec |

### Unified

| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/api/v1/ask` | Query anything — auto-detects spec type; `spec_id` always optional |
| `POST` | `/api/v1/call/{tool_name}` | Call a specific tool directly by name with explicit params |

### Auth

| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/api/v1/detect-auth` | Probe a URL without credentials — identify what auth is needed |
| `GET`  | `/api/v1/auth-status/{spec_id}` | Show whether credentials are configured (values never returned) |
| `POST` | `/api/v1/auth/invalidate/{spec_id}` | Force token refresh on the next call |

### Management

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET`  | `/api/v1/specs` | List all loaded specs with entity/operation counts |
| `GET`  | `/api/v1/files` | List all generated tool files on disk |
| `DELETE` | `/api/v1/files/{spec_id}` | Delete a tool file from disk |
| `GET`  | `/health` | Health check — uptime and loaded spec count |

---

## OData Quick Start

### 1. Load a spec

```bash
# Northwind v2 (public demo)
curl -s -X POST http://localhost:8080/api/v1/generate \
  -H "Content-Type: application/json" \
  -d '{"source": "https://services.odata.org/V2/Northwind/Northwind.svc/$metadata",
       "spec_id": "nw"}' | jq

# TripPin v4 (public demo)
curl -s -X POST http://localhost:8080/api/v1/generate \
  -H "Content-Type: application/json" \
  -d '{"source": "https://services.odata.org/TripPinRESTierService/$metadata",
       "spec_id": "trip"}' | jq

# With GPT-4o enriched docstrings (~30s)
curl -s -X POST http://localhost:8080/api/v1/generate \
  -H "Content-Type: application/json" \
  -d '{"source": "https://services.odata.org/TripPinRESTierService/$metadata",
       "spec_id": "trip",
       "use_ai_descriptions": true}' | jq
```

### 2. Query in natural language

```bash
# spec_id auto-detected when only one spec is loaded
curl -s -X POST http://localhost:8080/api/v1/query \
  -H "Content-Type: application/json" \
  -d '{"query": "Get top 5 most expensive products"}' | jq

# explicit spec_id (required when multiple specs are loaded)
curl -s -X POST http://localhost:8080/api/v1/query \
  -H "Content-Type: application/json" \
  -d '{"spec_id": "trip", "query": "Get person with username russellwhyte"}' | jq

# cap the number of records
curl -s -X POST http://localhost:8080/api/v1/query \
  -H "Content-Type: application/json" \
  -d '{"query": "List airports", "max_records": 5}' | jq
```

### 3. Call tools directly

Tool names follow the pattern `{spec_id}__{EntitySet}__{operation}`:

```bash
# filter + sort + field selection
curl -s -X POST http://localhost:8080/api/v1/call/nw__Products__list \
  -H "Content-Type: application/json" \
  -d '{"top": 5, "orderby": "UnitPrice desc",
       "select": "ProductID,ProductName,UnitPrice"}' | jq

# single record by key
curl -s -X POST http://localhost:8080/api/v1/call/nw__Products__get \
  -H "Content-Type: application/json" \
  -d '{"ProductID": 1}' | jq
```

---

## OpenAPI / Swagger Quick Start

### 1. Load a spec

```bash
# Swagger 2.0
curl -s -X POST http://localhost:8080/api/v1/generate/openapi \
  -H "Content-Type: application/json" \
  -d '{"source": "https://petstore.swagger.io/v2/swagger.json",
       "spec_id": "petstore"}' | jq

# OpenAPI 3.x
curl -s -X POST http://localhost:8080/api/v1/generate/openapi \
  -H "Content-Type: application/json" \
  -d '{"source": "https://petstore3.swagger.io/api/v3/openapi.json",
       "spec_id": "ps3",
       "base_url": "https://petstore3.swagger.io/api/v3"}' | jq
```

For **authenticated services** — see the [Auth section](#auth--secure-credential-handling) below. Credentials go in `.env`, not in the request body.

### 2. Query

```bash
# auto-detected spec
curl -s -X POST http://localhost:8080/api/v1/query/openapi \
  -H "Content-Type: application/json" \
  -d '{"query": "Get all available pets"}' | jq

# unified endpoint (works for both OData and OpenAPI)
curl -s -X POST http://localhost:8080/api/v1/ask \
  -H "Content-Type: application/json" \
  -d '{"query": "Find pets with status sold"}' | jq
```

### 3. Call tools directly

Tool names follow `{spec_id}__{operationId}`:

```bash
curl -s -X POST http://localhost:8080/api/v1/call/petstore__findPetsByStatus \
  -H "Content-Type: application/json" \
  -d '{"status": "available"}' | jq

curl -s -X POST http://localhost:8080/api/v1/call/petstore__getPetById \
  -H "Content-Type: application/json" \
  -d '{"petId": 1}' | jq
```

---

## Auth — Secure Credential Handling

Credentials are stored **only in `.env` on the server**. The LLM and API callers never provide or see credentials. Tokens are fetched and refreshed automatically at runtime.

### Supported auth types

| Type | `.env` value | Description |
|------|-------------|-------------|
| SAP XSUAA / OAuth2 | `xsuaa` or `oauth2` | Client-credentials flow; tokens auto-refreshed |
| Static bearer token | `bearer_static` | Fixed JWT/token — no refresh |
| HTTP Basic | `basic` | Username + password → Base64 Authorization header |
| API key (header) | `apikey` | Key injected into a request header |
| API key (query) | `apikey` | Key appended as a query parameter |
| API key (cookie) | `apikey` | Key sent as a cookie |
| None | `none` | Public APIs — no auth |

### Step 1 — Detect what auth is required

```bash
curl -s -X POST http://localhost:8080/api/v1/detect-auth \
  -H "Content-Type: application/json" \
  -d '{"url": "https://yourapp.cfapps.eu10.hana.ondemand.com",
       "spec_id": "myservice"}' | jq
```

The response shows the detected auth type and the exact env-var names to set:

```json
{
  "auth_required": true,
  "auth_type": "Bearer (SAP XSUAA)",
  "is_xsuaa": true,
  "configured": false,
  "env_vars_to_set": {
    "MYSERVICE_AUTH_TYPE":   "xsuaa",
    "MYSERVICE_AUTH_URL":    "<url from XSUAA service credentials>",
    "MYSERVICE_CLIENT_ID":   "<clientid>",
    "MYSERVICE_CLIENT_SECRET": "<clientsecret>"
  },
  "how_to_get_creds": "BTP Cockpit → Space → Service Instances → XSUAA → View Credentials"
}
```

### Step 2 — Add credentials to `.env`

The naming convention is `{SPEC_ID_UPPERCASE}_{VAR}`. Examples:

```env
# ── SAP BTP / XSUAA (OAuth2 client_credentials) ────────────────────────────
MYSERVICE_AUTH_TYPE=xsuaa
MYSERVICE_AUTH_URL=https://<subaccount>.authentication.eu10.hana.ondemand.com
MYSERVICE_CLIENT_ID=sb-app!t1234
MYSERVICE_CLIENT_SECRET=xxxxxxxxxxxxxxxx

# Optionally override the token endpoint (default: AUTH_URL/oauth/token)
MYSERVICE_TOKEN_URL=https://<subaccount>.authentication.eu10.hana.ondemand.com/oauth/token

# ── Static bearer token ─────────────────────────────────────────────────────
REPORTAPI_AUTH_TYPE=bearer_static
REPORTAPI_BEARER_TOKEN=eyJhbGciOiJSUzI1NiJ9...

# ── HTTP Basic auth ─────────────────────────────────────────────────────────
LEGACYAPI_AUTH_TYPE=basic
LEGACYAPI_USERNAME=admin
LEGACYAPI_PASSWORD=secret

# ── API key in a header ─────────────────────────────────────────────────────
WEATHERAPI_AUTH_TYPE=apikey
WEATHERAPI_API_KEY=abc123xyz
WEATHERAPI_API_KEY_NAME=X-API-Key
WEATHERAPI_API_KEY_IN=header       # header | query | cookie

# ── API key as query param ──────────────────────────────────────────────────
MAPSAPI_AUTH_TYPE=apikey
MAPSAPI_API_KEY=mymapskey
MAPSAPI_API_KEY_NAME=key
MAPSAPI_API_KEY_IN=query
```

Restart `api.py` — it loads `.env` on startup. For the MCP server, set these in `.env` the same way.

### Step 3 — Load the spec (no credentials needed in the request)

```bash
curl -s -X POST http://localhost:8080/api/v1/generate/openapi \
  -H "Content-Type: application/json" \
  -d '{"source": "https://yourapp.cfapps.eu10.hana.ondemand.com/api/openapi.json",
       "spec_id": "myservice"}' | jq
```

The server reads `MYSERVICE_AUTH_TYPE`, fetches an XSUAA token, and stores it internally. No credentials in the request.

### Step 4 — Query normally

```bash
curl -s -X POST http://localhost:8080/api/v1/ask \
  -H "Content-Type: application/json" \
  -d '{"query": "Get all open orders"}' | jq
```

Before every outgoing API call the executor checks whether the cached token is still valid (60-second buffer). If expired it silently fetches a new one and retries. On an unexpected 401 it invalidates the cache and retries once.

### Auth management

```bash
# See configuration status — credential values are never returned
curl -s http://localhost:8080/api/v1/auth-status/myservice | jq
# → {"configured": true, "auth_type": "xsuaa",
#    "token_status": {"cached": true, "expires_in_seconds": 43140}}

# Force token refresh before the next call
curl -s -X POST http://localhost:8080/api/v1/auth/invalidate/myservice | jq
```

---

## RBAC — Role-Based Access Control

Every operation is gated by role. The **default role is read-only** — it can only call `list` and `get` (OData) and `GET`/`HEAD` (HTTP). Write operations (`create`, `update`, `delete` / `POST`, `PUT`, `PATCH`, `DELETE`) require the **admin role**.

### Configuring roles (environment variables)

```env
# ── Option 1: single-role for the whole server instance ────────────────────
MCP_USER_ROLE=admin           # everyone on this instance is admin

# ── Option 2: tie role to the server's email identity ──────────────────────
MCP_USER_EMAIL=alice@acme.com
RBAC_EMAIL_ROLES=alice@acme.com:admin,bob@acme.com:default

# ── Option 3: grant a role to an entire domain ─────────────────────────────
RBAC_DOMAIN_ROLES=acme.com:admin,partner.com:default

# ── Option 4: opaque API keys (HTTP / SSE multi-user transport) ─────────────
RBAC_USERS=secret_adminkey123:admin,readonly_xyz:default
```

Resolution order (first match wins): `MCP_USER_ROLE` → email exact match → email domain → `X-API-Key` header → fallback `default`.

### Checking your role (MCP tool)

```
whoami           → shows role, permitted operations, and RBAC config
```

### Error response when role is insufficient

```json
{
  "error": "This operation requires the 'admin' role. Current role: 'default'. Set MCP_USER_ROLE=admin or add your email to RBAC_EMAIL_ROLES.",
  "required_role": "admin"
}
```

---

## Persistence — Auto-load on Restart

Every `/generate` call writes `tools/{spec_id}.py` that embeds the full spec as base64. On the next startup:

```
server starts
 └─ scans tools/*.py
 └─ decodes embedded spec (no network call needed)
 └─ restores all in-memory state (specs, tools)
 └─ server is query-ready immediately
```

Auth credentials are **not** stored in tool files. They are re-read from `.env` on every execute call.

---

## MCP Server — Claude Desktop / Claude Code

### Start

```bash
# stdio (Claude Desktop / Claude Code)
python3 mcp_server.py

# SSE transport on a custom port
python3 mcp_server.py --sse 8001
# → http://localhost:8001/sse

# Streamable-HTTP transport
python3 mcp_server.py --http 8002
# → http://localhost:8002/mcp
```

### Claude Desktop config (`~/.claude/claude_desktop_config.json`)

```json
{
  "mcpServers": {
    "odata-mcp": {
      "command": "python3",
      "args": ["/home/user/projects/mcp/mcp_server.py"]
    }
  }
}
```

### Static MCP tools (always available)

| Tool | Description |
|------|-------------|
| `load_odata_spec` | Load an OData `$metadata` URL and register CRUD tools |
| `load_openapi_spec` | Load an OpenAPI/Swagger spec and register operation tools |
| `smart_query` | Natural-language query over any loaded spec |
| `generate_tool_file` | Persist the current spec to `tools/{spec_id}.py` |
| `list_generated_tools` | List all dynamically registered tools |
| `list_tool_files` | List tool files on disk |
| `get_ai_insights` | Ask GPT-4o a question about a loaded spec |
| `generate_odata_query` | Convert natural language to an OData REST call |
| `test_ai_connection` | Ping SAP AI Core / GPT-4o |
| `whoami` | Show current role and permitted operations |

---

## How It Works

```
POST /api/v1/generate  (or load_odata_spec in MCP)
  └─ fetch spec from URL (follows redirects)
  └─ parse → EntitySets / operations, parameters, keys, security schemes
  └─ register tools in memory
  └─ write tools/{spec_id}.py (embedded spec as base64)

POST /api/v1/ask  (or smart_query in MCP)
  └─ resolve spec_id
  │   ├─ single spec loaded → use it automatically
  │   └─ multiple specs loaded → GPT-4o picks the right one
  └─ build compact spec summary (entity names, keys, fields)
  └─ GPT-4o returns JSON: { entity_set, operation, args, explanation }
  └─ RBAC check (write ops require admin role)
  └─ executor called with resolved args

OpenAPI executor
  └─ auth_manager.get_auth_headers(spec_id)
  │    └─ reads {SPEC_ID}_AUTH_TYPE from env
  │    └─ for xsuaa/oauth2: fetch token (client_credentials)
  │    └─ cache token with 60-second expiry buffer
  │    └─ return { Authorization: "Bearer <token>" }
  └─ route args by location: path / query / header / body / cookie
  └─ build URL: base_url + path (with path-param substitution)
  └─ HTTP call
  └─ on 401: invalidate cache → refresh token → retry once

OData executor
  └─ build OData URL: base_url/EntitySet(key)?$filter=...&$top=...
  └─ for write ops: fetch X-CSRF-Token first (SAP OData v2 requirement)
  └─ POST/PATCH/DELETE with CSRF token + cookies
  └─ return parsed JSON
```

### Tool naming convention

```
OData    → {spec_id}__{EntitySet}__{operation}
           nw__Products__list     nw__Products__get
           nw__Products__create   nw__Products__update   nw__Products__delete

OpenAPI  → {spec_id}__{operationId}
           petstore__findPetsByStatus   petstore__getPetById   petstore__addPet
```

---

## SAP AI Core Configuration

```env
SAP_AI_API_URL=https://api.ai.prod.eu-central-1.aws.ml.hana.ondemand.com
SAP_AI_CLIENT_ID=<client_id from AI Core service binding>
SAP_AI_CLIENT_SECRET=<client_secret>
SAP_AI_AUTH_URL=https://<subaccount>.authentication.eu10.hana.ondemand.com
SAP_AI_RESOURCE_GROUP=default
SAP_AI_MODEL_NAME=gpt-4o

# Pin to a specific deployment (skips auto-discovery at startup)
SAP_AI_DEPLOYMENT_ID=d5c7fe212eec831c
```

The client auto-discovers the right deployment ID from `/v2/lm/deployments` if `SAP_AI_DEPLOYMENT_ID` is not set. It prefers `gpt-4o` → `gpt-4.1` → `gpt-4` → first RUNNING deployment.

---

## Environment Variables — Full Reference

### SAP AI Core

| Variable | Description |
|----------|-------------|
| `SAP_AI_API_URL` | AI Core API base URL |
| `SAP_AI_CLIENT_ID` | OAuth2 client ID |
| `SAP_AI_CLIENT_SECRET` | OAuth2 client secret |
| `SAP_AI_AUTH_URL` | XSUAA auth URL (without `/oauth/token`) |
| `SAP_AI_RESOURCE_GROUP` | AI Core resource group (default: `default`) |
| `SAP_AI_MODEL_NAME` | Model name (default: `gpt-4o`) |
| `SAP_AI_DEPLOYMENT_ID` | Pin to a specific deployment ID |

### RBAC

| Variable | Description |
|----------|-------------|
| `MCP_USER_ROLE` | `admin` or `default` — server-wide role override |
| `MCP_USER_EMAIL` | Email to look up in `RBAC_EMAIL_ROLES` |
| `RBAC_EMAIL_ROLES` | `email:role,email:role,...` — exact email → role |
| `RBAC_DOMAIN_ROLES` | `domain:role,...` — domain-level catch-all |
| `RBAC_USERS` | `apikey:role,...` — opaque API keys for HTTP transport |

### Per-spec auth (`{PREFIX}` = spec_id uppercased, hyphens → underscores)

| Variable | Description |
|----------|-------------|
| `{PREFIX}_AUTH_TYPE` | `xsuaa` / `oauth2` / `basic` / `apikey` / `bearer_static` / `none` |
| `{PREFIX}_AUTH_URL` | XSUAA / OAuth2 auth URL |
| `{PREFIX}_CLIENT_ID` | OAuth2 client ID |
| `{PREFIX}_CLIENT_SECRET` | OAuth2 client secret |
| `{PREFIX}_GRANT_TYPE` | OAuth2 grant type (default: `client_credentials`) |
| `{PREFIX}_TOKEN_URL` | Override the token endpoint |
| `{PREFIX}_BEARER_TOKEN` | Static bearer token |
| `{PREFIX}_USERNAME` | Basic auth username |
| `{PREFIX}_PASSWORD` | Basic auth password |
| `{PREFIX}_API_KEY` | API key value |
| `{PREFIX}_API_KEY_NAME` | API key header/param name (default: `X-API-Key`) |
| `{PREFIX}_API_KEY_IN` | Where to send the key: `header` / `query` / `cookie` |

---

## Troubleshooting

| Symptom | Fix |
|---------|-----|
| `port already in use` | `lsof -ti:8080 \| xargs kill -9` |
| `No specs loaded` | First run: call `/generate`. Subsequent runs: tools auto-load from `tools/*.py`. |
| AI Core 404 on inference | Set `SAP_AI_DEPLOYMENT_ID` to the hex deployment ID shown in BTP cockpit. |
| 401 on API call | Run `POST /api/v1/detect-auth` → set env vars → restart server. |
| 401 persists after setting env vars | Token may be stale: `POST /api/v1/auth/invalidate/{spec_id}` |
| XSUAA token fetch fails | Verify `AUTH_URL` has no trailing slash; `GRANT_TYPE=client_credentials`. |
| CSRF error on OData write | Handled automatically — executor fetches `X-CSRF-Token` before write ops. |
| TripPin returns 0 results | Session URL changes on each access — `/generate` follows redirects automatically. |
| Write op denied (role error) | Set `MCP_USER_ROLE=admin` in `.env` or add your email to `RBAC_EMAIL_ROLES`. |
| `permission denied` on MCP | Call the `whoami` MCP tool to see your current role and how to upgrade it. |