Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
12 changes: 6 additions & 6 deletions .github/workflows/sql-sanitizer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ jobs:

- name: Verify plugins is responding
run: |
curl -sf -X GET http://localhost:4444/admin/plugins/stats \
curl -sf -X GET http://localhost:4444/v1/admin/plugins/stats \
-H "Authorization: Bearer ${{ steps.generate_jwt.outputs.token }}" \
-H "Content-Type: application/json" | yq

Expand Down Expand Up @@ -202,7 +202,7 @@ jobs:

# Register gateway
echo "Registering fast_time gateway..."
GATEWAY_RESPONSE=$(curl -s -X POST http://localhost:4444/gateways \
GATEWAY_RESPONSE=$(curl -s -X POST http://localhost:4444/v1/gateways \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
Expand All @@ -215,7 +215,7 @@ jobs:
# Extract gateway ID (handle existing gateway)
if echo "$GATEWAY_RESPONSE" | grep -q "already exists"; then
echo "Gateway already exists, fetching existing ID..."
GATEWAYS=$(curl -s http://localhost:4444/gateways \
GATEWAYS=$(curl -s http://localhost:4444/v1/gateways \
-H "Authorization: Bearer $TOKEN")
GATEWAY_ID=$(echo "$GATEWAYS" | jq -r '.[] | select(.name=="fast_time") | .id')
else
Expand All @@ -226,7 +226,7 @@ jobs:
# Wait for tools to sync
echo "Waiting for tools to sync..."
for i in {1..30}; do
TOOLS=$(curl -s http://localhost:4444/tools \
TOOLS=$(curl -s http://localhost:4444/v1/tools \
-H "Authorization: Bearer $TOKEN")
TOOL_COUNT=$(echo "$TOOLS" | jq --arg gid "$GATEWAY_ID" '[.[] | select(.gatewayId==$gid)] | length')
if [ "$TOOL_COUNT" -gt 0 ]; then
Expand All @@ -238,7 +238,7 @@ jobs:
sleep 2
done

TOOLS=$(curl -s http://localhost:4444/tools \
TOOLS=$(curl -s http://localhost:4444/v1/tools \
-H "Authorization: Bearer $TOKEN")
TOOL_IDS=$(echo "$TOOLS" | jq --arg gid "$GATEWAY_ID" -c '[.[] | select(.gatewayId==$gid) | .id]')
echo "Tool IDs: $TOOL_IDS"
Expand All @@ -250,7 +250,7 @@ jobs:

# Create virtual server
echo "Creating virtual server..."
SERVER_RESPONSE=$(curl -s -X POST http://localhost:4444/servers \
SERVER_RESPONSE=$(curl -s -X POST http://localhost:4444/v1/servers \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{
Expand Down
10 changes: 5 additions & 5 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"files": "(?x)( package-lock\\.json$ |Cargo\\.lock$ |uv\\.lock$ |go\\.sum$ |mcpgateway/sri_hashes\\.json$ )|^.secrets.baseline$",
"lines": null
},
"generated_at": "2026-04-27T19:52:22Z",
"generated_at": "2026-04-28T08:13:50Z",
"plugins_used": [
{
"name": "AWSKeyDetector"
Expand Down Expand Up @@ -4844,7 +4844,7 @@
"hashed_secret": "c377074d6473f35a91001981355da793dc808ffd",
"is_secret": false,
"is_verified": false,
"line_number": 753,
"line_number": 751,
"type": "Hex High Entropy String",
"verified_result": null
}
Expand All @@ -4862,7 +4862,7 @@
"hashed_secret": "ff37a98a9963d347e9749a5c1b3936a4a245a6ff",
"is_secret": false,
"is_verified": false,
"line_number": 2420,
"line_number": 2421,
"type": "Secret Keyword",
"verified_result": null
}
Expand Down Expand Up @@ -9262,15 +9262,15 @@
"hashed_secret": "6745fa570d36be08400efa1cbc2f057bb001290e",
"is_secret": false,
"is_verified": false,
"line_number": 2523,
"line_number": 2554,
"type": "Hex High Entropy String",
"verified_result": null
},
{
"hashed_secret": "4f13f134744a2fadbbe2d624687246347d12fa63",
"is_secret": false,
"is_verified": false,
"line_number": 2811,
"line_number": 2842,
"type": "Hex High Entropy String",
"verified_result": null
}
Expand Down
161 changes: 161 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Migration Guide — API v1 Prefix

**PR:** [#4403 feat: API endpoint is served under the `/v1` prefix](https://github.com/IBM/mcp-context-forge/pull/4403)
**Branch:** `API_v1` → `main`
**Type:** Breaking change

---

## Overview

This release introduces API versioning for ContextForge. Most resource management and business-logic endpoints are now served under the `/v1` URL prefix. A new `mcpgateway/api/v1/__init__.py` module centralises router assembly via a `build_v1_router()` factory, keeping `main.py` clean and making future version additions (`/v2`, etc.) straightforward.

Protocol-level routes, infrastructure-compatible routes, and some maintenance APIs intentionally remain unversioned at the root. These either follow external standards, must stay stable for infrastructure compatibility, or are mounted separately from the main versioned router.

---

## Breaking Change

Most previously unversioned resource-management paths are now served under `/v1`. Clients must update their base URLs accordingly.

**Affected paths (previously at root, now under `/v1`):**

`/tools`, `/resources`, `/prompts`, `/gateways`, `/servers`, `/roots`, `/metrics`, `/tags`, `/a2a`, `/admin`, `/auth`, `/teams`, `/tokens`, `/rbac`, `/observability`, `/cancellation`, `/toolops`, `/reverse-proxy`, `/export`, `/import`

**Important exception:** not every metrics-related route becomes `/v1/metrics/**`. Metrics maintenance endpoints remain available at `/api/metrics/**`, and may also be exposed under `/v1/api/metrics/**` when enabled.

---

## Endpoint Classification

### Versioned — now served under `/v1`

| Endpoint Group | Old Path | New Path | Feature Flag |
|---|---|---|---|
| MCP Protocol | `/protocol/**` | `/v1/protocol/**` | always-on |
| Tools | `/tools/**` | `/v1/tools/**` | always-on |
| Resources | `/resources/**` | `/v1/resources/**` | always-on |
| Prompts | `/prompts/**` | `/v1/prompts/**` | always-on |
| Gateways | `/gateways/**` | `/v1/gateways/**` | always-on |
| Roots | `/roots/**` | `/v1/roots/**` | always-on |
| Servers | `/servers/**` | `/v1/servers/**` | always-on |
| Metrics | `/metrics/**` | `/v1/metrics/**` | always-on |
| Tags | `/tags/**` | `/v1/tags/**` | always-on |
| Export / Import | `/export`, `/import` | `/v1/export`, `/v1/import` | always-on |
| Tool Plugin Bindings | `/tools/plugin_bindings/**` | `/v1/tools/plugin_bindings/**` | always-on |
| Admin UI & API | `/admin/**` | `/v1/admin/**` | `MCPGATEWAY_ADMIN_API_ENABLED` |
| Runtime Admin | `/admin/runtime/**` | `/v1/admin/runtime/**` | `MCPGATEWAY_ADMIN_API_ENABLED` |
| LLM Admin | `/admin/llm/**` | `/v1/admin/llm/**` | `MCPGATEWAY_LLMCHAT_ENABLED` |
| A2A | `/a2a/**` | `/v1/a2a/**` | `MCPGATEWAY_A2A_ENABLED` |
| Observability | `/observability/**` | `/v1/observability/**` | `OBSERVABILITY_ENABLED` |
| Reverse Proxy | `/reverse-proxy/**` | `/v1/reverse-proxy/**` | `MCPGATEWAY_REVERSE_PROXY_ENABLED` |
| Tool Cancellation | `/cancellation/**` | `/v1/cancellation/**` | `MCPGATEWAY_TOOL_CANCELLATION_ENABLED` |
| ToolOps | `/toolops/**` | `/v1/toolops/**` | `TOOLOPS_ENABLED` |
| Authentication | `/auth/**` | `/v1/auth/**` | `EMAIL_AUTH_ENABLED` |
| Email Authentication | `/auth/email/**` | `/v1/auth/email/**` | `EMAIL_AUTH_ENABLED` |
| SSO Authentication | `/auth/sso/**` | `/v1/auth/sso/**` | `EMAIL_AUTH_ENABLED` + `SSO_ENABLED` |
| Teams | `/teams/**` | `/v1/teams/**` | `EMAIL_AUTH_ENABLED` |
| JWT Token Catalog | `/tokens/**` | `/v1/tokens/**` | `EMAIL_AUTH_ENABLED` |
| RBAC | `/rbac/**` | `/v1/rbac/**` | `EMAIL_AUTH_ENABLED` |
| LLM Chat | `/llmchat/**` | `/v1/llmchat/**` | `MCPGATEWAY_LLMCHAT_ENABLED` |
| LLM Config | `/llm/**` | `/v1/llm/**` | `MCPGATEWAY_LLMCHAT_ENABLED` |
| Metrics maintenance | `/api/metrics/**` | `/v1/api/metrics/**` | `metrics_cleanup_enabled` or `metrics_rollup_enabled` |

### Not versioned — remain at root (unchanged)

| Endpoint | Path | Reason |
|---|---|---|
| Health probes | `/health`, `/ready`, `/health/security` | Infrastructure / liveness; must remain stable |
| MCP Streamable HTTP transport | `/mcp` | MCP protocol spec — path is fixed by the spec |
| Internal MCP transport bridge | `/_internal/mcp/transport` | Internal trusted bridge; not a public API |
| OAuth 2.0 | `/oauth/**` | Standard protocol location (RFC 6749) |
| Well-known URIs | `/.well-known/**` | RFC 8615 / RFC 9116 / RFC 9728 — path is standardised |
| Per-server well-known | `/servers/{id}/.well-known/**` | RFC standard path; must not be prefixed |
| Version / Diagnostics | `/version` | Diagnostic utility, not a resource API |
| Static assets | `/static/**` | UI asset serving |
| Root redirect | `/` | Entry point / UI redirect |
| Favicon | `/favicon.ico` | Browser convention |
| Log Search | `/api/logs/**` | Internal structured-logging query interface |
| Metrics maintenance | `/api/metrics/**` | Operational maintenance API remains mounted at root |
| LLM Proxy | `{LLM_API_PREFIX}` (default `/v1`) | Prefix is runtime-configurable via `LLM_API_PREFIX`; it is mounted directly on `app`, not nested inside the gateway's own `/v1` router, even when the configured prefix is also `/v1` |

---

## Migration Steps

### 1. Update all client base URLs

Replace any hardcoded unversioned paths in your client code, configuration files, CI scripts, or SDK wrappers:

```diff
- GET /tools
+ GET /v1/tools

- POST /gateways
+ POST /v1/gateways

- GET /servers
+ GET /v1/servers

- GET /admin/
+ GET /v1/admin/

- POST /auth/login
+ POST /v1/auth/login
```

### 2. Update environment variables or base-URL constants

If you configure a base URL such as `MCPGATEWAY_BASE_URL=https://example.com`, ensure downstream consumers append `/v1/` before any resource segment:

```diff
- https://example.com/tools/list
+ https://example.com/v1/tools/list
```

### 3. Update API clients / SDKs

Any SDK or HTTP client wrapper that prepends a path prefix must be updated to use `/v1`:

```python
# Before
client = MCPGatewayClient(base_url="https://example.com")
client.get("/tools")

# After
client = MCPGatewayClient(base_url="https://example.com/v1")
client.get("/tools")
# or
client.get("/v1/tools")
```

### 4. Update reverse-proxy / load-balancer rules

If you have Nginx, HAProxy, or cloud load-balancer rules that route on path prefixes, add `/v1` to resource-path matchers. Health, OAuth, well-known, `/mcp`, `/api/logs/**`, and `/api/metrics/**` paths do not change.

### 5. Migrate test paths (automated helper)

A utility script is provided to migrate Python test file path references automatically:

```bash
python scripts/update_test_paths.py
```

This script rewrites path strings such as `"/tools"` → `"/v1/tools"` across Python test files under `tests/` while skipping paths that are intentionally unversioned (`.well-known`, `/oauth`, `/mcp`, `/health`, `/api/logs`, `/api/metrics`, etc.).

---

## Code Changes (Summary)

| File | Change |
|---|---|
| `mcpgateway/api/v1/__init__.py` | **New** — `build_v1_router()` factory that assembles all versioned routers under `/v1`; feature-flagged routers are conditionally included here |
| `mcpgateway/api/__init__.py` | **New** — namespace package |
| `mcpgateway/main.py` | Router registration refactored; versioned routers delegated to `build_v1_router()`; unversioned routers mounted directly on `app` |
| `mcpgateway/admin.py` | Admin redirect paths updated to `/v1/admin/*` |
| `mcpgateway/middleware/path_filter.py` | Path references updated to `/v1` prefixed patterns |
| `mcpgateway/middleware/rbac.py` | Path references updated to `/v1` prefixed patterns |
| `mcpgateway/middleware/token_scoping.py` | Path references updated; `/v1` prefix stripped for internal pattern matching |
| `mcpgateway/routers/metrics_maintenance.py` | Metrics maintenance endpoints remain mounted at `/api/metrics/**` and are also conditionally included under `/v1/api/metrics/**` |
| `tests/` | Python test paths are migrated toward `/v1` prefixed resource endpoints |
| `scripts/update_test_paths.py` | **New** — utility script for migrating Python test path references under `tests/` |
14 changes: 7 additions & 7 deletions charts/mcp-stack/files/ocp/locustfile_mcp_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -960,10 +960,10 @@ def rpc_list_tools(self):
"""tools/list via /rpc (REST JSON-RPC)."""
payload = _jsonrpc("tools/list", {})
with self.client.post(
"/rpc",
"/v1/rpc",
data=json.dumps(payload),
headers=self._auth_headers,
name="REST /rpc tools/list",
name="REST /v1/rpc tools/list",
catch_response=True,
) as resp:
if resp.status_code == 200:
Expand All @@ -987,10 +987,10 @@ def rpc_call_tool(self):
args = {}
payload = _jsonrpc("tools/call", {"name": tool, "arguments": args})
with self.client.post(
"/rpc",
"/v1/rpc",
data=json.dumps(payload),
headers=self._auth_headers,
name="REST /rpc tools/call",
name="REST /v1/rpc tools/call",
catch_response=True,
) as resp:
if resp.status_code == 200:
Expand All @@ -1001,11 +1001,11 @@ def rpc_call_tool(self):
@task(5)
@tag("baseline", "rest", "list")
def rest_list_tools(self):
"""/tools via REST API."""
"""/v1/tools via REST API."""
with self.client.get(
"/tools",
"/v1/tools",
headers=self._auth_headers,
name="REST /tools",
name="REST /v1/tools",
catch_response=True,
) as resp:
if resp.status_code == 200:
Expand Down
Loading
Loading