Metadata-Version: 2.4
Name: glean-mcp-proxy
Version: 0.1.0
Summary: Expose OpenAPI endpoints to MCP
Author: David Grimm
Author-email: David.Grimm@wellsfargo.com
Requires-Python: >=3.12.0,<4.0.0
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Dist: aiohttp
Requires-Dist: cachetools
Requires-Dist: fastapi
Requires-Dist: fastmcp (>=3.1.1,<4.0.0)
Requires-Dist: mcp
Requires-Dist: mcp[cli]
Requires-Dist: openapi-spec-validator (>=0.7.2,<0.8.0)
Requires-Dist: pydantic (>=2.0,<3.0)
Requires-Dist: pyjwt (>=2.0,<3.0)
Requires-Dist: python-dotenv
Requires-Dist: requests
Requires-Dist: tomli (>=2.0,<3.0)
Requires-Dist: uvicorn
Description-Content-Type: text/markdown

# OpenAPI MCP Proxy

A flexible, production-ready proxy that dynamically exposes OpenAPI-described REST APIs as **Model Context Protocol (MCP) tools**. Automatically keep your MCP surface in sync with any REST API without manual tool definitions.

**Author**: David Grimm <david.grimm@wellsfargo.com>

---

## Table of Contents

- [Features](#features)
- [Quick Start](#quick-start)
- [Installation](#installation)
- [Configuration](#configuration)
  - [TOML Format](#toml-format)
  - [Server Settings](#server-settings)
  - [OpenAPI Specs](#openapi-specs)
  - [Path Parameters](#path-parameters)
  - [Filtering by HTTP Method](#filtering-by-http-method)
- [Usage](#usage)
  - [stdio Transport](#stdio-transport)
  - [HTTP Transport](#http-transport)
  - [Framework Selection](#framework-selection)
  - [Tool Naming and Startup Summary](#tool-naming-and-startup-summary)
- [Authentication](#authentication)
  - [Token-Based Authentication](#token-based-authentication)
  - [OAuth/JWT Authentication](#oauthjwt-authentication)
  - [Dual-Mode Authentication](#dual-mode-authentication)
  - [Roles and RBAC](#roles-and-rbac)
  - [Disabling Authentication](#disabling-authentication)
- [Architecture](#architecture)
- [Testing & Coverage](#testing--coverage)
- [Troubleshooting](#troubleshooting)
- [Development](#development)

---

## Features

- **Dynamic Tool Generation** — Automatically create MCP tools from OpenAPI specs  
- **Multi-Spec Support** — Proxy multiple APIs simultaneously  
- **Dual Transport** — stdio (default) or HTTP with optional HTTPS/TLS  
- **Framework Flexibility** — FastMCP or Tachyon MCP (configurable)  
- **Static & OAuth Auth** — Support both static bearer tokens and JWT/OAuth2  
- **Path Parameter Templating** — Set default values per spec  
- **HTTP Method Filtering** — Expose only specific methods (GET, POST, etc.)  
- **Readable Tool Names** — Uses OpenAPI `operationId` when available (with clean fallback naming)  
- **Startup Tool Summary** — Logs per-spec tool inventory (name, method, path, summary) at boot  
- **Proxy Support** — Route through corporate HTTP proxies  
- **Per-Spec Overrides** — Token, base URL, proxy per API  
- **SSL/TLS** — HTTPS, mutual TLS, and certificate validation  
- **Type-Safe Configuration** — Pydantic v2 validation  

---

## Quick Start

### 1. Install

```bash
# For FastMCP (default, recommended)
poetry install --extras fastmcp

# OR for Tachyon MCP
poetry install --extras tachyon
```

### 2. Create a configuration file (`config.toml`)

```toml
[server]
use_tachyon = false          # Use FastMCP (true for Tachyon)
transport = "stdio"          # or "http"
host = "0.0.0.0"
port = 8080
verify_ssl = true

[[specs]]
url = "file:///path/to/spec.json"      # Can be file://, http://, or https://
token = "your-bearer-token"             # Optional: for APIs requiring auth
path_types = ["get", "post"]            # Optional: filter by HTTP method

[specs.path_params]                      # Optional: default values for path params
environment_id = "my-env-123"
```

### 3. Run

```bash
# Use config.toml in current directory
mcp_proxy

# Or specify config path
mcp_proxy --config /path/to/config.toml

# Or set env var
export MCP_PROXY_CONFIG=/path/to/config.toml
mcp_proxy
```

### ✅ Success!

Your MCP tools are now available. Connect with:
- **VS Code Copilot**: Point to stdio endpoint
- **OpenAI API**: Use HTTP transport at `http://localhost:8080`
- **MCP Inspector**: `mcp inspect stdio "mcp_proxy"` (testable)

---

## Installation

### Prerequisites

- Python 3.12 or later
- Poetry (https://python-poetry.org/docs/#installation)

### From Source

```bash
git clone <repository>
cd App-emap-lib-mcp-proxy

# FastMCP (default, recommended)
poetry install --extras fastmcp

# Tachyon MCP (enterprise)
poetry install --extras tachyon

# All frameworks
poetry install --extras all

# Add development/testing tools to any of the above
poetry install --extras fastmcp --with dev
```

### Verify Installation

```bash
poetry run mcp_proxy --help
# Output:
# usage: mcp_proxy [-h] [-c PATH]
#
# OpenAPI MCP Proxy — expose OpenAPI endpoints as MCP tools
#
# options:
#   -h, --help            show this help message and exit
#   -c PATH, --config PATH
#                         Path to config TOML file (overrides MCP_PROXY_CONFIG env var)
```

---

## Configuration

### Configuration Discovery

The proxy looks for configuration in this order:

1. **Command-line argument**: `mcp_proxy --config /path/to/config.toml`
2. **Environment variable**: `export MCP_PROXY_CONFIG=/path/to/config.toml`
3. **Current directory**: `./config.toml`
4. **Home directory**: `~/.mcp_proxy/config.toml`
5. **Environment fallback**: Uses legacy environment variables if no TOML is found

### Environment Fallback Format (Legacy)

If no TOML config file is found via auto-discovery, the proxy falls back to environment variables.

The required spec source is `OPENAPI_SPEC_URLS`.

`OPENAPI_SPEC_URLS` supports these formats:

1. **JSON array (recommended)**

```bash
export OPENAPI_SPEC_URLS='[
  {"url":"file:///tmp/spec-a.json","path_params":{"environment_id":"prod"}},
  "https://api.example.com/openapi.json"
]'
```

2. **Comma-separated list**

```bash
export OPENAPI_SPEC_URLS="file:///tmp/spec-a.json,https://api.example.com/openapi.json"
```

3. **Comma-separated with line continuation**

```bash
export OPENAPI_SPEC_URLS="file:///tmp/spec-a.json,\\
 https://api.example.com/openapi.json"
```

Accepted JSON array items:

- String URL/path: `"file:///tmp/spec.json"`
- Object with `SpecConfig` fields: `{"url":"...","token":"...","proxy":"...","base_url":"...","path_types":[...],"path_params":{...}}`

Other environment variables used in fallback mode:

- `USE_TACHYON` (default `true`)
- `MCP_TRANSPORT` (default `stdio`)
- `MCP_SERVER_HOST` (default `0.0.0.0`)
- `MCP_SERVER_PORT` (default `8080`)
- `VERIFY_SSL` (default `true`)
- `SPEC_FETCH_TIMEOUT_SECONDS` (default `10`)
- `UPSTREAM_REQUEST_TIMEOUT_SECONDS` (default `30`)
- `UPSTREAM_MAX_RETRIES` (default `3`)
- `UPSTREAM_RETRY_BACKOFF_SECONDS` (default `0.5`)
- `CIRCUIT_BREAKER_ENABLED` (default `true`)
- `CIRCUIT_BREAKER_FAILURE_THRESHOLD` (default `5`)
- `CIRCUIT_BREAKER_RECOVERY_SECONDS` (default `30`)

Note: Spec payloads are JSON-only. `.yaml` / `.yml` specs and non-JSON responses are rejected at startup.

### TOML Format

All configuration uses **TOML** (Tom's Obvious, Minimal Language). Start with this template:

```toml
[server]
# Framework selection
use_tachyon = false              # false = FastMCP, true = Tachyon MCP

# Transport mode
transport = "stdio"              # "stdio" (default, local) or "http" (remote/API)
host = "0.0.0.0"                # HTTP server listen address
port = 8080                       # HTTP server port (1-65535)

# SSL/TLS Configuration (optional, HTTP transport only)
verify_ssl = true                # Verify upstream API certificates
# ssl_keyfile = "/path/to/server.key"      # Enable HTTPS
# ssl_certfile = "/path/to/server.crt"     # Enable HTTPS
# ssl_ca_certs = "/path/to/ca-bundle.crt"  # Mutual TLS client verification
# ssl_keyfile_passphrase = "secret"        # If key file is encrypted

# Timeout settings
spec_fetch_timeout_seconds = 10.0          # Timeout for fetching OpenAPI specs
upstream_request_timeout_seconds = 30.0    # Timeout for proxied upstream API calls

# Retry / backoff (upstream API calls only)
upstream_max_retries = 3                   # 0 = no retries
upstream_retry_backoff_seconds = 0.5       # Doubles each attempt (0.5s, 1s, 2s, …)

# Circuit breaker
circuit_breaker_enabled = true             # Fast-fail after repeated upstream failures
circuit_breaker_failure_threshold = 5      # Consecutive failures before circuit opens
circuit_breaker_recovery_seconds = 30.0    # Seconds before a half-open probe is allowed

[[specs]]
# OpenAPI specification
url = "file:///path/to/spec.json"     # file://, http://, or https://
# url = "https://api.example.com/openapi.json"

# Optional: short name prefix for tool names (overrides spec info.title)
# name = "myapi"                        # Tools become: myapi_get_users, myapi_post_orders, etc.

# Optional: credentials for fetching spec or upstream API
token = "Bearer my-token"              # If spec URL or API requires auth
proxy = "http://proxy.example.com:8080"  # Route through corporate proxy

# Optional: override base URL from spec
base_url = "https://api.example.com"

# Optional: filter which HTTP methods to expose as tools
path_types = ["get", "post", "put", "delete"]  # Defaults to all methods

# Optional: default values for templated path parameters
[specs.path_params]
environment_id = "prod-env-123"
region = "us-east-1"

# Add more specs by repeating [[specs]] section
[[specs]]
url = "file:///path/to/spec2.json"
path_types = ["get"]
```

### Server Settings

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `use_tachyon` | bool | `false` | Framework: `true` = Tachyon, `false` = FastMCP |
| `transport` | string | `"stdio"` | `"stdio"` (stdio MCP) or `"http"` (HTTP server) |
| `host` | string | `"0.0.0.0"` | HTTP listen address (ignored for stdio) |
| `port` | integer | `8080` | HTTP port, 1–65535 (ignored for stdio) |
| `verify_ssl` | bool | `true` | Verify upstream API SSL certificates |
| `spec_fetch_timeout_seconds` | float | `10.0` | Timeout in seconds for fetching OpenAPI specs at startup |
| `upstream_request_timeout_seconds` | float | `30.0` | Timeout in seconds for each proxied upstream API call |
| `upstream_max_retries` | integer | `3` | Retry attempts on timeout/connection error (`0` = no retries) |
| `upstream_retry_backoff_seconds` | float | `0.5` | Initial backoff between retries; doubles each attempt (0.5s → 1s → 2s …) |
| `circuit_breaker_enabled` | bool | `true` | Fast-fail requests when an upstream is repeatedly failing |
| `circuit_breaker_failure_threshold` | integer | `5` | Consecutive failures before the circuit opens |
| `circuit_breaker_recovery_seconds` | float | `30.0` | Seconds before the circuit allows a probe request (half-open) |
| `ssl_keyfile` | string | — | Path to TLS private key (enables HTTPS) |
| `ssl_certfile` | string | — | Path to TLS certificate (PEM/CRT format) |
| `ssl_ca_certs` | string | — | Path to CA bundle for client certificate verification |
| `ssl_keyfile_passphrase` | string | — | Passphrase for encrypted TLS key file |

### OpenAPI Specs

Each `[[specs]]` section defines one OpenAPI API to proxy:

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `url` | string | ✅ | OpenAPI spec location: `file://`, `http://`, or `https://` |
| `name` | string | — | Override the API name prefix used in all tool names (e.g., `"bigpanda"`) |
| `token` | string | — | Bearer token for API or spec authentication |
| `proxy` | string | — | HTTP proxy URL for upstream requests |
| `base_url` | string | — | Override the API `servers[0].url` from spec |
| `path_types` | list | — | Filter HTTP methods: `["get", "post", ...]` |
| `path_params` | dict | — | Default values for path parameters |

The `name` field is useful when the spec's `info.title` is verbose or version-stamped (e.g., `"Alert Pipeline API v1.3.0-SNAPSHOT"`), which would otherwise produce unwieldy tool names. Setting `name = "alerts"` keeps tools short and readable: `alerts_get_incidents` instead of `alert_pipeline_api_v1_3_0_snapshot_get_incidents`.

#### Spec URL Resolution

**Relative paths** are resolved intelligently:

- **Bare relative path** (e.g., `openapi/spec.json`):
  - First tries relative to **current working directory** (CWD)
  - Falls back to relative to **config file directory**
  - **Use this for specs in your project repository**

- **Explicit dot-relative** (e.g., `./openapi/spec.json` or `../specs/api.json`):
  - Always resolved relative to **config file directory**
  - **Use this for specs outside your CWD**

**Examples**:
```toml
# Current directory strategy
[[specs]]
url = "openapi/bigpanda.json"    # Tries ./openapi/bigpanda.json first

# Explicit relative to config directory
[[specs]]
url = "../shared-specs/api.json" # Relative to config file location

# Absolute or remote
[[specs]]
url = "/etc/mcp-proxy/specs/api.json"
[[specs]]
url = "https://api.example.com/openapi.json"
```

### Path Parameters

**Template path parameters** with default values:

```toml
[[specs]]
url = "file:///path/to/spec.json"

[specs.path_params]
environment_id = "prod-123"      # Replaces {environment_id} in paths
account_id = "acc-456"           # Can template multiple values
region = "us-east-1"
```

When a tool is invoked, these defaults are used if the caller doesn't provide the value.

### Filtering by HTTP Method

**Expose only specific HTTP methods**:

```toml
[[specs]]
url = "file:///path/to/spec.json"
path_types = ["get"]               # Only GET operations → tools

[[specs]]
url = "file:///path/to/spec.json"
path_types = ["get", "post"]       # GET and POST only

# Omit path_types to expose all methods
[[specs]]
url = "file:///path/to/spec.json"
```

---

## Usage

### stdio Transport (Default)
**Purpose**: MCP over standard input/output (`stdio`). Used by:
- VS Code Copilot Chat
- Claude Desktop
- MCP Inspector (local testing)
### Dynamic Spec Management
- Any MCP client connecting via stdout/stdin

**Setup**:
```toml
[server]
transport = "stdio"     # or omit (default)
```

**Run**:
```bash
mcp_proxy --config config.toml
```

**In VS Code Copilot**:
Add to `.vscode/settings.json` or `codeowners.json`:
```json
{
  "github.copilot.agent.endpoints": [
    {
      "codespaceId": "my-project",
      "provider": "stdio",
      "command": "mcp_proxy",
      "args": ["--config", "/path/to/config.toml"]
    }
  ]
}
```

### HTTP Transport

**Purpose**: MCP over HTTP. Used by:
- Remote clients via REST API
- Web applications
- Services that can't use stdio

**Setup**:
```toml
[server]
transport = "http"
host = "0.0.0.0"       # Listen on all interfaces
port = 8080            # Or your preferred port
```

**Run**:
```bash
mcp_proxy --config config.toml
```

**HTTP Endpoints (FastMCP)**:
- `POST /mcp/` — MCP Streamable HTTP endpoint (tool discovery + invocation via MCP protocol)
- `GET /sse/` — SSE transport endpoint (when using SSE transport)

Note: In FastMCP HTTP mode, the startup banner may display `/mcp` (without trailing slash).
Use MCP clients against `/mcp/`; legacy `/tools` and `/call` routes are not used.

**Quick connectivity check**:
```bash
curl -i http://localhost:8080/mcp/
```

**Enable HTTPS (TLS)**:
```toml
[server]
transport = "http"
port = 443
ssl_keyfile = "/etc/ssl/private/server.key"
ssl_certfile = "/etc/ssl/certs/server.crt"
```

**Mutual TLS (mTLS)**:
```toml
[server]
ssl_keyfile = "/etc/ssl/private/server.key"
ssl_certfile = "/etc/ssl/certs/server.crt"
ssl_ca_certs = "/etc/ssl/certs/ca-bundle.crt"  # Client cert verification
```

### Framework Selection

**FastMCP** (default, recommended):
- Lightweight, async-first
- Fewer dependencies
- Better performance
- Install: `poetry install --extras fastmcp`

```toml
[server]
use_tachyon = false   # FastMCP
```

**Tachyon MCP** (enterprise, optional):
- Integrations with LangChain
- Advanced enterprise features
- Install: `poetry install --extras tachyon`

```toml
[server]
use_tachyon = true    # Tachyon MCP

```

### Tool Naming and Startup Summary

Tool names are generated to stay readable while remaining unique across specs.

- Preferred source: OpenAPI `operationId` (camelCase converted to snake_case)
- Fallback source: method + normalized path segments
- Multi-spec collisions: de-duplicated by suffix (`_2`, `_3`, ...)

Example names:

- `bigpanda_search_incidents`
- `bigpanda_get_incident`
- `bigpanda_list_alert_tags`

At startup, the proxy logs a per-spec inventory including:

- Spec name and source URL
- Tool count for that spec
- Tool name, method, path, and short description/summary

Example header:

```text
========================================================================
MCP Proxy Startup Summary - 11 tool(s) registered
========================================================================
```

---

## Authentication

Authentication is **optional and disabled by default**. Enable only if your API or tool invocation requires it.

### Token-Based Authentication

**Use static bearer tokens** (pre-shared, hashed tokens):

#### 1. Generate a token hash

Use the included `token` utility:

```bash
# Generate a new random token (prints token + hash + save reminder)
token

# Generate and include a ready-to-paste TOML snippet for a user
token --user svc-readonly

# Hash a specific known token value
token --token "my-secret-token-12345"

# Output hash only (for scripting)
token --quiet

# Control token length (default 32 bytes = 64-char hex token)
token --length 48
```

Example output:
```
Token:      3f8a2c1d...  ← save this; used in Authorization header
Token hash: sha256:abc123def456...  ← put this in config TOML

IMPORTANT: Save the token value now — it cannot be recovered from the hash.
```

#### 2. Configure in TOML

```toml
[auth]
enabled = true
mode = "static"            # Only static tokens
header_name = "Authorization"
require_bearer_prefix = true

[[auth.users]]
user_id = "svc-readonly"
display_name = "Service Readonly"
roles = ["reader"]
scopes = ["proxy.read"]

[[auth.tokens]]
token_hash = "sha256:abc123def456..."   # From step 1
user_id = "svc-readonly"                # Links to auth.users
scopes = ["proxy.read"]
active = true
# expires_at = "2027-12-31T23:59:59Z"  # Optional expiration
```

#### 3. Call tools with token

```bash
# HTTP transport (MCP endpoint)
curl -i http://localhost:8080/mcp/ \
  -H "Authorization: Bearer my-secret-token-12345"
```

Use an MCP client (VS Code Copilot, MCP Inspector, SDK client) to enumerate and invoke tools over `/mcp/`.

### OAuth/JWT Authentication

**Use JWT tokens** (signed, expiring credentials):

#### 1. Configure JWT issuer

```toml
[auth]
enabled = true
mode = "oauth"                           # Only OAuth/JWT
header_name = "Authorization"
require_bearer_prefix = true

[auth.oauth]
enabled = true
issuer = "https://auth.example.com"     # JWT issuer
audience = "mcp-proxy"                  # Expected audience (`aud`)
algorithms = ["HS256"]                  # Signed with HMAC-SHA256
shared_secret_env = "MCP_OAUTH_SHARED_SECRET"  # Env var holding secret
```

#### 2. Provide JWT secret

```bash
export MCP_OAUTH_SHARED_SECRET="your-shared-secret"
mcp_proxy --config config.toml
```

Or in production, use a `.env` file (not committed):
```bash
# .env.local (add to .gitignore)
MCP_OAUTH_SHARED_SECRET=your-production-secret
```

#### 3. Generate a JWT

```python
import jwt
import json
from datetime import datetime, timedelta, timezone

payload = {
    "iss": "https://auth.example.com",
    "aud": "mcp-proxy",
    "sub": "user-123",
    "exp": datetime.now(timezone.utc) + timedelta(hours=1)
}
token = jwt.encode(payload, "your-shared-secret", algorithm="HS256")
print(token)
```

#### 4. Call tools with JWT

```bash
curl -i http://localhost:8080/mcp/ \
  -H "Authorization: Bearer <your-jwt-token>"
```

### Dual-Mode Authentication

**Accept either static tokens OR JWT** (both simultaneously):

```toml
[auth]
enabled = true
mode = "either"                 # Accept static OR OAuth

[[auth.users]]
user_id = "svc-bot"
roles = ["reader"]

[[auth.tokens]]
token_hash = "sha256:abc123..."  # Static token
user_id = "svc-bot"
active = true

[auth.oauth]
enabled = true
issuer = "https://auth.example.com"
audience = "mcp-proxy"
algorithms = ["HS256"]
shared_secret_env = "MCP_OAUTH_SHARED_SECRET"
```

Now the proxy accepts **either**:
- Requests with header `Authorization: Bearer my-static-token`
- Requests with header `Authorization: Bearer <jwt-token>`

### Roles and RBAC

**Roles enforce which HTTP methods a user can invoke.** Role names map directly to HTTP method names.

| Role | Permits |
|------|---------|
| `get` | GET tools (read-only) |
| `post` | POST tools (create) |
| `put` | PUT tools (replace) |
| `patch` | PATCH tools (update) |
| `delete` | DELETE tools (destroy) |

**Rules:**
- A user with an **empty roles list** is **unrestricted** — can call any tool
- A user with roles can only call tools whose HTTP method is in their role list
- Role names are **case-insensitive** (`GET`, `get`, `Get` all match)
- A 403 Forbidden is returned when a role check fails (upstream API is never called)

**Example — tiered service accounts:**
```toml
[[auth.users]]
user_id = "svc-readonly"
roles = ["get"]                      # May only call GET tools

[[auth.users]]
user_id = "svc-writer"
roles = ["get", "post", "patch"]     # Read + create + update, no delete

[[auth.users]]
user_id = "svc-admin"
roles = []                           # Unrestricted — all methods permitted
```

**For OAuth/JWT**, roles are read from the `roles` claim in the token payload:
```json
{
  "sub": "user-123",
  "roles": ["get", "post"],
  "exp": 1234567890
}
```

### Disabling Authentication

```toml
[auth]
enabled = false   # or omit the entire [auth] section
```

When disabled:
- All HTTP requests are accepted
- No Authorization header required
- All tools available to all callers

---

## Architecture

### How It Works

```
┌─────────────────────┐
│   MCP Client        │  (VS Code, Claude Desktop, etc.)
└──────────┬──────────┘
           │
    ┌──────▼──────────────────────┐
    │  OpenAPI MCP Proxy           │
    │  ┌────────────────────────┐  │
    │  │ Transport Layer        │  │  stdio or HTTP
    │  │ (stdio or FastMCP HTTP)│  │
    │  └──────────┬─────────────┘  │
    │             │                 │
    │  ┌──────────▼──────────────┐  │
    │  │ Auth Enforcement        │  │  (if enabled)
    │  │ (static token/JWT)      │  │
    │  └──────────┬──────────────┘  │
    │             │                 │
    │  ┌──────────▼──────────────┐  │
    │  │ OpenAPI Spec Loader     │  │
    │  │ (validates & parses)    │  │
    │  └──────────┬──────────────┘  │
    │             │                 │
    │  ┌──────────▼──────────────┐  │
    │  │ Tool Registration       │  │  (one tool per operation)
    │  │ (filters by path_types)│  │
    │  └──────────┬──────────────┘  │
    │             │                 │
    │  ┌──────────▼──────────────┐  │
    │  │ HTTP Proxy              │  │  Route to upstream API
    │  │ (template paths,auth)   │  │
    │  └──────────┬──────────────┘  │
    └─────────────┼──────────────────┘
                  │
         ┌────────▼────────┐
         │ Upstream REST API│  (BigPanda, custom services, etc.)
         └─────────────────┘
```

### Key Components

**1. Config Loader** (`app_config.py`)
- Reads TOML config with Pydantic validation
- Handles env var fallbacks
- Discovers config file in standard locations

**2. Spec Manager** (`openapi_mcp_proxy.py`)
- Fetches OpenAPI specs (file://, http://, https://)
- Validates against OpenAPI 3.0/3.1 schema
- Caches specs to avoid re-fetching

**3. Tool Builder** (`openapi_mcp_proxy.py`)
- For each operation in the OpenAPI spec, registers one MCP tool
- Filters by `path_types` (HTTP method)
- Injects `path_params` defaults

**4. HTTP Proxy**
- Routes tool invocations to upstream API
- Handles template parameters
- Preserves headers, authentication
- Returns JSON-serializable results

---

## Testing & Coverage

### Run Tests

```bash
# Install including dev dependencies
poetry install --extras fastmcp --with dev

# Run all tests
poetry run pytest

# Run with coverage
poetry run pytest --cov=mcp_proxy --cov-report=html

# Run specific test file
poetry run pytest tests/test_openapi_builder_unit.py -v

# Run specific test function
poetry run pytest tests/test_openapi_builder_unit.py::test_builder_initialization -v
```

### Coverage Goals

Current test coverage: **85%** of `mcp_proxy/` module.

**Coverage by file**:
- `openapi_mcp_proxy.py`: 85% (core proxy logic)
- `app_config.py`: 85%+ (config loading)
- `models.py`: 90%+ (Pydantic models)

**What's tested**:
- Configuration parsing (valid, invalid, edge cases)
- Spec loading (local files, remote HTTP, invalid specs)
- Tool registration (filtering, parameter templating)
- Path resolution (CWD vs config-dir relative paths)
- Authentication (static tokens, JWT/OAuth)
- HTTP proxy routing (headers, auth, error cases)
- Startup behavior (graceful errors, exit codes)

### Test Files

```
tests/
├── test_openapi_builder_unit.py     # Builder, routing, auth (38 tests)
├── test_app_config_unit.py          # Config loading (8 tests)
└── conftest.py                      # Pytest fixtures (optional)
```

---

## Troubleshooting

### Issue: Config file not found

**Error**:
```
RuntimeError: Config file not found or invalid
```

**Solutions**:
1. Check file exists: `ls -la config.toml`
2. Use absolute path: `mcp_proxy --config /full/path/to/config.toml`
3. Set env var: `export MCP_PROXY_CONFIG=/path/to/config.toml`
4. Verify TOML syntax: `python -c "import tomli; tomli.load(open('config.toml', 'rb'))"`

### Issue: OpenAPI spec fails to load

**Error**:
```
RuntimeError: Failed to load spec from file://...
```

**Solutions**:
1. Verify file exists: `ls -la openapi/spec.json`
2. Check file permissions: `cat openapi/spec.json` (should output JSON)
3. Use absolute path: `url = "/full/path/to/spec.json"`
4. Validate spec syntax: `python -c "import json; json.load(open('openapi/spec.json'))"`

### Issue: Path parameters not working

**Problem**: Tool receives `{environment_id}` instead of actual value

**Solution**: Define in config:
```toml
[[specs]]
url = "..."

[specs.path_params]
environment_id = "prod-123"
```

### Issue: Authentication fails with 401

**Error**: All requests return `{"error": "Unauthorized"}`

**Solutions**:

**For static tokens**:
1. Check `auth.enabled = true`
2. Verify token hash: `python -c "import hashlib; print('sha256:' + hashlib.sha256(b'YOUR_TOKEN').hexdigest())"`
3. Ensure token matches in `[[auth.tokens]]`
4. Check header format: `Authorization: Bearer YOUR_TOKEN` (not just `Bearer`)

**For OAuth/JWT**:
1. Check `auth.oauth.enabled = true`
2. Verify JWT secret: `echo $MCP_OAUTH_SHARED_SECRET`
3. Ensure token is valid JWT: `python -c "import jwt; jwt.decode(token, secret, algorithms=['HS256'])"`
4. Check issuer and audience match config

### Issue: Same token works locally, fails in production

**Likely cause**: Different config files or env vars in production.

**Debug**:
```bash
# Check which config is loaded
mcp_proxy --config config.toml 2>&1 | grep "Loading config"

# Verify env vars
echo $MCP_PROXY_CONFIG
echo $MCP_OAUTH_SHARED_SECRET
```

### Issue: Proxy hangs or times out

**Possible causes**:
1. Upstream API unreachable
2. Corporate proxy requires authentication (add `proxy` in `[[specs]]`)
3. Network latency
4. Default timeouts too short for slow APIs

**Solutions**:
```bash
# Test upstream API directly
curl -H "Bearer YOUR_TOKEN" https://api.example.com/health

# Check proxy connectivity
curl -x http://corporate-proxy:8080 https://api.example.com
```

**Tune timeouts and retries** in `config.toml`:
```toml
[server]
spec_fetch_timeout_seconds = 30.0          # Default 10 — increase for slow spec endpoints
upstream_request_timeout_seconds = 60.0    # Default 30 — increase for slow upstream APIs
upstream_max_retries = 5                   # Default 3 — more retries for flaky upstreams
upstream_retry_backoff_seconds = 1.0       # Default 0.5 — longer initial wait between retries
circuit_breaker_failure_threshold = 10     # Default 5 — raise if upstreams are normally unreliable
circuit_breaker_recovery_seconds = 60.0    # Default 30 — longer recovery window
```

### Issue: `UnicodeDecodeError` or encoding issues

**Error**:
```
UnicodeDecodeError: 'utf-8' codec can't decode byte...
```

**Solution**: Update to Python 3.12+:
```bash
python --version  # Should be 3.12.0 or later
```

---

## Development

### Project Structure

```
App-emap-lib-mcp-proxy/
├── mcp_proxy/
│   ├── __main__.py               # CLI entry point
│   ├── __init__.py               # Package init
│   ├── models.py                 # Pydantic config models
│   ├── app_config.py             # Config loading logic
│   └── openapi_mcp_proxy.py      # Core proxy builder & HTTP server
├── config/
│   └── bp.toml                   # Example configuration
├── openapi/
│   └── bp_openapi.json           # Example OpenAPI spec
├── tests/
│   ├── test_openapi_builder_unit.py
│   ├── test_app_config_unit.py
│   └── conftest.py
├── docs/
│   └── CONFIGURATION.md          # Configuration reference
├── pyproject.toml                # Project metadata & dependencies
├── requirements.txt              # Pip requirements
└── README.md                     # This file
```

### Adding a New OpenAPI Spec

1. **Obtain the OpenAPI spec**:
   ```bash
   # Download to local file
   curl https://api.example.com/openapi.json -o openapi/my-api.json
   
   # Or, keep remote
   # (URL will be fetched at startup)
   ```

2. **Add to config**:
   ```toml
   [[specs]]
   url = "openapi/my-api.json"
   token = "bearer-token-if-needed"
   path_types = ["get", "post"]
   ```

3. **Test**:
   ```bash
   mcp_proxy --config config.toml
   # Check logs for "Registered X tools from my-api.json"
   ```

### Creating Custom Authentication

To add new auth methods beyond static tokens and OAuth:

1. Edit `mcp_proxy/models.py` — add new `AuthXyzConfig` model
2. Edit `mcp_proxy/openapi_mcp_proxy.py` — add `_authenticate_xyz()` method
3. Update `_authorize_http_caller()` to call new method
4. Add tests in `tests/test_openapi_builder_unit.py`

### Running Tests Locally

```bash
# Full suite
pytest tests/ -v

# With coverage report
pytest tests/ --cov=mcp_proxy --cov-report=term-missing

# Generate HTML coverage report
pytest tests/ --cov=mcp_proxy --cov-report=html
# Open htmlcov/index.html in browser
```

### Linting & Type Checking

```bash
# Type check (if mypy available)
poetry run mypy mcp_proxy/

# Format code
poetry run black mcp_proxy/ tests/

# Lint
poetry run flake8 mcp_proxy/ tests/
```

### Building & Packaging

```bash
# Create distribution
poetry build

# Publish to PyPI (requires authentication)
poetry publish

# Install locally from source (choose a framework)
poetry install --extras fastmcp

# Install with all frameworks and dev tools
poetry install --extras all --with dev
```

---

## License

Proprietary — Wells Fargo Confidential

---

## Support

For issues or questions:
1. Check [Troubleshooting](#troubleshooting) section
2. Review logs: `mcp_proxy --config config.toml 2>&1 | tail -50`
3. Open an issue with config (redact secrets), error message, and steps to reproduce

