> **Note**: The Anthropic API version (v0.1) is defined as a constant `ANTHROPIC_API_VERSION` in `registry/constants.py`. All code references this constant rather than hardcoding the version string. ---
Scanned 6/1/2026
Install via CLI
openskills install tools-only/X-Skills# Anthropic MCP Registry API - Implementation Guide
> **Note**: The Anthropic API version (v0.1) is defined as a constant `ANTHROPIC_API_VERSION` in `registry/constants.py`. All code references this constant rather than hardcoding the version string.
---
## Overview
This implementation provides full compatibility with the [Anthropic MCP Registry REST API v0.1 specification](https://github.com/modelcontextprotocol/registry), enabling seamless integration with MCP ecosystem tools and downstream applications.
### Key Features
- ✅ **3 REST API endpoints** for server discovery
- ✅ **JWT Bearer token authentication** via Keycloak
- ✅ **Cursor-based pagination** for server lists
- ✅ **Permission-based filtering** using MCP scopes
- ✅ **Complete Pydantic models** matching Anthropic spec
- ✅ **Automatic data transformation** from internal format
---
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Client (Authorization: Bearer <JWT>) │
└────────────────────┬────────────────────────────────────────┘
│ HTTP Request
▼
┌─────────────────────────────────────────────────────────────┐
│ Nginx (:80/:443) │
│ └─ /v0.1/* location │
│ └─ auth_request /validate ────────────────┐ │
└────────────────────┬───────────────────────────┼────────────┘
│ │
│ ▼
│ ┌─────────────────────────┐
│ │ Auth Server (:8888) │
│ │ - Validates JWT │
│ │ - Checks Keycloak │
│ │ - Returns headers │
│ └─────────────┬───────────┘
│ │
│ ◄──────────────────────────┘
│ X-User, X-Scopes, X-Username
▼
┌─────────────────────────────────────────────────────────────┐
│ Registry FastAPI (:7860) │
│ ├─ nginx_proxied_auth() - Reads headers │
│ ├─ registry_routes.py - API endpoints │
│ ├─ server_service - Data access │
│ └─ transform_service - Format conversion │
└────────────────────┬────────────────────────────────────────┘
│
▼
Anthropic Schema Response
```
---
## File Structure
### New Files
| File | Purpose |
|------|---------|
| `registry/constants.py` | Anthropic API constants (`ANTHROPIC_SERVER_NAMESPACE`, limits) |
| `registry/schemas/anthropic_schema.py` | 9 Pydantic models for Anthropic spec |
| `registry/services/transform_service.py` | Data transformation between formats |
| `registry/api/registry_routes.py` | 3 REST endpoints with JWT auth |
| `tests/unit/api/test_registry_routes.py` | API endpoint tests |
| `tests/unit/services/test_transform_service.py` | Transformation tests |
| `docs/design/anthropic-api-test-commands.md` | 20 test scenarios with curl |
### Modified Files
| File | Changes |
|------|---------|
| `registry/main.py` | Registered v0.1 router |
| `registry/auth/dependencies.py` | Added `nginx_proxied_auth()` function |
| `docker/nginx_rev_proxy_*.conf` | Added `/v0.1/` location with auth validation |
| `.gitignore` | Added `tests/reports/` |
---
## Constants Configuration
All hardcoded values are centralized in `registry/constants.py`:
```python
class RegistryConstants(BaseModel):
# Anthropic Registry API v0.1 constants
ANTHROPIC_SERVER_NAMESPACE: str = "io.mcpgateway"
ANTHROPIC_API_DEFAULT_LIMIT: int = 100
ANTHROPIC_API_MAX_LIMIT: int = 1000
```
**Usage**: Import with `from ..constants import REGISTRY_CONSTANTS`
---
## API Endpoints
### 1. List Servers
```
GET /v0.1/servers?cursor={cursor}&limit={limit}
```
**Purpose**: List all MCP servers the authenticated user can access.
**Query Parameters**:
- `cursor` (optional): Pagination cursor from previous response
- `limit` (optional): Results per page (1-1000, default 100)
**Response**: `ServerList` with pagination metadata
**Example**:
```bash
curl "http://localhost/v0.1/servers?limit=5" \
-H "Authorization: Bearer $TOKEN"
```
### 2. List Server Versions
```
GET /v0.1/servers/{serverName:path}/versions
```
**Purpose**: List all available versions for a specific server.
**URL Parameters**:
- `serverName`: URL-encoded name (e.g., `io.mcpgateway%2Ffininfo`)
**Response**: `ServerList` (currently single version per server)
**Important**: Note `:path` route converter to handle `/` in server names.
**Example**:
```bash
curl "http://localhost/v0.1/servers/io.mcpgateway%2Ffininfo/versions" \
-H "Authorization: Bearer $TOKEN"
```
### 3. Get Server Version Details
```
GET /v0.1/servers/{serverName:path}/versions/{version}
```
**Purpose**: Get detailed information for a specific server version.
**URL Parameters**:
- `serverName`: URL-encoded name (e.g., `io.mcpgateway%2Ffininfo`)
- `version`: Version string (use `latest` for current version)
**Response**: `ServerResponse` with full server details
**Example**:
```bash
curl "http://localhost/v0.1/servers/io.mcpgateway%2Ffininfo/versions/latest" \
-H "Authorization: Bearer $TOKEN"
```
---
## Authentication Flow
### 1. JWT Bearer Token Validation
**Client → Nginx**:
```
GET /v0.1/servers
Authorization: Bearer eyJhbGci...
```
**Nginx → Auth Server** (`/validate` endpoint):
```
GET /validate
X-Authorization: Bearer eyJhbGci...
X-Original-URL: http://localhost/v0.1/servers
```
**Auth Server Processing**:
1. Validates JWT signature using Keycloak JWKS
2. Checks expiration, issuer (3-tier validation), audience
- Tries external URL: `https://mcpgateway.ddns.net/realms/mcp-gateway`
- Tries internal URL: `http://keycloak:8080/realms/mcp-gateway`
- Tries localhost URL: `http://localhost:8080/realms/mcp-gateway`
3. Extracts user info: `preferred_username`, `groups`, `scope`
4. Maps Keycloak groups to MCP scopes
**Auth Server → Nginx** (response headers):
```
X-User: service-account-mcp-gateway-m2m
X-Username: service-account-mcp-gateway-m2m
X-Scopes: mcp-servers-unrestricted/read mcp-servers-unrestricted/execute
X-Auth-Method: keycloak
```
**Nginx → FastAPI**:
```
GET /v0.1/servers
X-User: service-account-mcp-gateway-m2m
X-Scopes: mcp-servers-unrestricted/read mcp-servers-unrestricted/execute
Authorization: Bearer eyJhbGci...
```
### 2. nginx Configuration
**Critical Setup** in `/v0.1/` location block:
```nginx
location /v0.1/ {
# Authenticate via auth-server
auth_request /validate;
# Capture auth server response headers
auth_request_set $auth_user $upstream_http_x_user;
auth_request_set $auth_username $upstream_http_x_username;
auth_request_set $auth_scopes $upstream_http_x_scopes;
auth_request_set $auth_method $upstream_http_x_auth_method;
# Forward to FastAPI with auth context
proxy_pass http://127.0.0.1:7860/v0.1/;
proxy_set_header X-User $auth_user;
proxy_set_header X-Username $auth_username;
proxy_set_header X-Scopes $auth_scopes;
proxy_set_header X-Auth-Method $auth_method;
proxy_set_header Authorization $http_authorization;
}
```
**Key Fix**: `/validate` endpoint must forward `Authorization` as `X-Authorization`:
```nginx
location = /validate {
proxy_pass http://auth-server:8888/validate;
# CRITICAL: Read from $http_authorization (client's Authorization header)
proxy_set_header X-Authorization $http_authorization;
}
```
### 3. FastAPI Authentication Dependency
**Function**: `nginx_proxied_auth()` in `registry/auth/dependencies.py`
**Supports Two Modes**:
1. **JWT Flow** (primary): Reads nginx headers from auth validation
2. **Cookie Flow** (fallback): Reads session cookies for backward compatibility
```python
def nginx_proxied_auth(
request: Request,
session: Cookie = None,
x_user: Header = None,
x_username: Header = None,
x_scopes: Header = None,
x_auth_method: Header = None,
) -> Dict[str, Any]:
# Try nginx headers first (JWT Bearer token)
if x_user or x_username:
username = x_username or x_user
scopes = x_scopes.split() if x_scopes else []
# Map scopes to groups
if 'mcp-servers-unrestricted/read' in scopes:
groups = ['mcp-registry-admin']
else:
groups = ['mcp-registry-user']
# Get accessible servers from scopes
accessible_servers = get_user_accessible_servers(scopes)
return {
'username': username,
'groups': groups,
'scopes': scopes,
'accessible_servers': accessible_servers,
'is_admin': 'mcp-registry-admin' in groups,
# ... more fields
}
# Fallback to session cookie
return enhanced_auth(session)
```
---
## Permission Checks
### Scope-Based Access Control
**IMPORTANT**: v0.1 API uses `accessible_servers` (MCP scopes), NOT `accessible_services` (UI scopes).
```python
# CORRECT - Check against accessible_servers
accessible_servers = user_context.get("accessible_servers", [])
if server_name not in accessible_servers:
raise HTTPException(404, "Server not found")
```
**Why**:
- `accessible_services` = UI-level services ("auth_server", "mcpgw")
- `accessible_servers` = MCP server names ("fininfo", "currenttime")
- M2M tokens have MCP scopes but no UI scopes
### User Context Structure
```python
{
"username": "service-account-mcp-gateway-m2m",
"groups": ["mcp-registry-admin"],
"scopes": [
"mcp-servers-unrestricted/read",
"mcp-servers-unrestricted/execute",
"mcp-servers-restricted/read",
"mcp-servers-restricted/execute"
],
"auth_method": "keycloak",
"provider": "keycloak",
"accessible_servers": [
"currenttime", "fininfo",
"mcpgw", "realserverfaketools", "sre-gateway"
],
"accessible_services": [], # Empty for M2M tokens
"is_admin": True,
"can_modify_servers": False
}
```
---
## Data Transformation
### Namespace Convention
**Internal Format**: `/fininfo`, `/currenttime/`
**Anthropic Format**: `io.mcpgateway/fininfo`, `io.mcpgateway/currenttime`
**Implementation** (`transform_service.py`):
```python
def _create_server_name(server_info: Dict[str, Any]) -> str:
path = server_info.get("path", "")
clean_path = path.strip("/")
namespace = REGISTRY_CONSTANTS.ANTHROPIC_SERVER_NAMESPACE
return f"{namespace}/{clean_path}"
```
### Server Detail Transformation
```python
def transform_to_server_detail(server_info: Dict[str, Any]) -> ServerDetail:
# Create Anthropic-format name
name = _create_server_name(server_info)
# Build package with transport config
transport = _create_transport_config(server_info)
package = Package(
registryType="mcpb",
identifier=name,
version="1.0.0",
transport=transport,
runtimeHint="docker"
)
# Add internal metadata
namespace = REGISTRY_CONSTANTS.ANTHROPIC_SERVER_NAMESPACE
meta = {
f"{namespace}/internal": {
"path": server_info.get("path"),
"is_enabled": server_info.get("is_enabled"),
"health_status": server_info.get("health_status"),
"num_tools": server_info.get("num_tools"),
"tags": server_info.get("tags", []),
"license": server_info.get("license", "N/A")
}
}
return ServerDetail(name=name, packages=[package], meta=meta, ...)
```
### Response Structure
```json
{
"server": {
"name": "io.mcpgateway/fininfo",
"description": "Financial Information",
"version": "1.0.0",
"title": "Financial Info",
"packages": [
{
"registryType": "mcpb",
"identifier": "io.mcpgateway/fininfo",
"version": "1.0.0",
"transport": {
"type": "streamable-http",
"url": "http://fininfo-server:8001/mcp/"
},
"runtimeHint": "docker"
}
],
"_meta": {
"io.mcpgateway/internal": {
"path": "/fininfo",
"is_enabled": true,
"health_status": "healthy",
"num_tools": 5,
"tags": ["Finance", "Stocks", "Market"],
"license": "MIT"
}
}
},
"_meta": {
"io.mcpgateway/registry": {
"last_checked": "2025-10-12T19:25:09.378358+00:00",
"health_status": "healthy"
}
}
}
```
---
## Pagination
### Cursor-Based Implementation
**Algorithm** (`transform_service.py`):
```python
def transform_to_server_list(
servers_data: List[Dict[str, Any]],
cursor: Optional[str] = None,
limit: Optional[int] = None
) -> ServerList:
# Apply defaults
limit = limit or REGISTRY_CONSTANTS.ANTHROPIC_API_DEFAULT_LIMIT
limit = min(limit, REGISTRY_CONSTANTS.ANTHROPIC_API_MAX_LIMIT)
# Sort alphabetically for consistency
sorted_servers = sorted(servers_data, key=lambda s: _create_server_name(s))
# Find cursor position
start_index = 0
if cursor:
for idx, server in enumerate(sorted_servers):
if _create_server_name(server) == cursor:
start_index = idx + 1
break
# Slice page
end_index = start_index + limit
page_servers = sorted_servers[start_index:end_index]
# Determine next cursor
has_more = end_index < len(sorted_servers)
next_cursor = _create_server_name(sorted_servers[end_index - 1]) if has_more else None
# Transform and return
return ServerList(
servers=[transform_to_server_response(s) for s in page_servers],
metadata=PaginationMetadata(nextCursor=next_cursor, count=len(page_servers))
)
```
**Example Flow**:
```
Page 1: GET /v0.1/servers?limit=3
← Returns: servers A, B, C with nextCursor="C"
Page 2: GET /v0.1/servers?cursor=C&limit=3
← Returns: servers D, E, F with nextCursor="F"
Page 3: GET /v0.1/servers?cursor=F&limit=3
← Returns: servers G, H with nextCursor=null (end)
```
---
## Critical Implementation Details
### 1. Route Path Parameters
**Problem**: Server names contain `/` which breaks FastAPI routing.
**Solution**: Use `:path` converter in route definition.
```python
# WRONG - Returns 404 for io.mcpgateway/fininfo
@router.get("/servers/{serverName}/versions")
# CORRECT - Captures full path including /
@router.get("/servers/{serverName:path}/versions")
```
**Why**: FastAPI URL-decodes before routing. `io.mcpgateway%2Ffininfo` becomes `io.mcpgateway/fininfo`, which looks like extra path segments without `:path`.
### 2. Trailing Slash Handling
**Problem**: Some servers have trailing slashes (`/currenttime/`), some don't (`/fininfo`).
**Solution**: Try both forms when looking up servers.
```python
# Construct path from server name
lookup_path = "/" + decoded_name.replace(expected_prefix, "")
# Try with and without trailing slash
server_info = server_service.get_server_info(lookup_path)
if not server_info:
server_info = server_service.get_server_info(lookup_path + "/")
# Use actual path from server_info for health checks
path = server_info.get("path", lookup_path) # Has correct trailing slash
health_data = health_service._get_service_health_data(path)
```
**Why**: Health data is indexed by exact path. Wrong path returns `"unknown"` status.
### 3. Namespace Constant Usage
**All occurrences** of hardcoded `"io.mcpgateway"` replaced with constant:
```python
from ..constants import REGISTRY_CONSTANTS
namespace = REGISTRY_CONSTANTS.ANTHROPIC_SERVER_NAMESPACE
expected_prefix = f"{namespace}/" # "io.mcpgateway/"
```
**Files using constant**:
- `registry/api/registry_routes.py` - Validates server name format
- `registry/services/transform_service.py` - Creates names and metadata keys
---
## Testing
### Generate Token
```bash
# Generate fresh credentials (tokens expire after 5 minutes)
./generate_creds.sh
# Load token
export TOKEN=$(jq -r '.access_token' .oauth-tokens/ingress.json)
# Verify token loaded
echo "Token: ${TOKEN:0:50}..."
```
### Test Endpoints
```bash
# 1. List servers with pagination
curl "http://localhost/v0.1/servers?limit=5" \
-H "Authorization: Bearer $TOKEN" | jq
# 2. List versions for a server (note %2F = /)
curl "http://localhost/v0.1/servers/io.mcpgateway%2Ffininfo/versions" \
-H "Authorization: Bearer $TOKEN" | jq
# 3. Get specific version details
curl "http://localhost/v0.1/servers/io.mcpgateway%2Ffininfo/versions/latest" \
-H "Authorization: Bearer $TOKEN" | jq
# 4. Test pagination
curl "http://localhost/v0.1/servers?limit=2" \
-H "Authorization: Bearer $TOKEN" | jq '.metadata'
# Get nextCursor and use it:
curl "http://localhost/v0.1/servers?cursor=io.mcpgateway%2Fcurrenttime&limit=2" \
-H "Authorization: Bearer $TOKEN" | jq
```
### Comprehensive Test Suite
See [docs/design/anthropic-api-test-commands.md](anthropic-api-test-commands.md) for 20 test scenarios.
---
## Common Issues & Solutions
### Issue: 404 on versions endpoint
**Symptom**: `GET /v0.1/servers/io.mcpgateway%2Ffininfo/versions` returns 404
**Cause**: Missing `:path` in route parameter
**Solution**: Ensure route uses `{serverName:path}` not `{serverName}`
### Issue: Health data shows "unknown"
**Symptom**: `health_status: "unknown"`, `last_checked: null`
**Cause**: Trailing slash mismatch in path lookup
**Solution**: Use `server_info.get("path")` for health checks, not constructed path
### Issue: Empty server list
**Symptom**: `{"servers": [], "metadata": {"count": 0}}`
**Cause**: Checking `accessible_services` instead of `accessible_servers`
**Solution**: Use `user_context["accessible_servers"]` for permission checks
### Issue: 401 Unauthorized
**Symptom**: `{"detail": "Token has expired"}`
**Cause**: JWT token expired (5 minute lifetime)
**Solution**: Run `./generate_creds.sh` to get fresh token
### Issue: Token not forwarded
**Symptom**: Auth server logs show `Authorization=False`
**Cause**: nginx using `$http_x_authorization` instead of `$http_authorization`
**Solution**: Update `/validate` location to use `$http_authorization`
---
## Schema Compliance
**OpenAPI Spec**: https://github.com/modelcontextprotocol/registry/blob/main/docs/reference/api/openapi.yaml
**Pydantic Models** (`registry/schemas/anthropic_schema.py`):
- ✅ `ServerList` - Paginated server list
- ✅ `ServerResponse` - Single server with metadata
- ✅ `ServerDetail` - Complete server information
- ✅ `Package` - Distribution package details
- ✅ `Transport` - Union of transport types
- ✅ `Repository` - Source code repository info
- ✅ `PaginationMetadata` - Cursor and count
- ✅ `ErrorResponse` - Error details
**Field Aliases**: Pydantic handles `_meta` fields with `Field(alias="_meta")`
---
## Next Steps
1. ✅ **JWT Authentication** - Fully implemented
2. ✅ **Permission Filtering** - Uses MCP scopes
3. ✅ **Health Data** - Includes status and last checked
4. ✅ **Pagination** - Cursor-based with configurable limits
5. 🔄 **Read-Only API Access** - Optional: Create dedicated M2M client with minimal scopes (see `.scratchpad/registry-api-readonly-access.md`)
6. 🔄 **Rate Limiting** - Future: Add per-client rate limits
7. 🔄 **Caching** - Future: Cache server list responses
---
## References
- **Issue**: [#175 - Support Anthropic MCP Registry REST API v0](https://github.com/agentic-community/mcp-gateway-registry/issues/175)
- **OpenAPI Spec**: https://github.com/modelcontextprotocol/registry/blob/main/docs/reference/api/openapi.yaml
- **API Guide**: https://github.com/modelcontextprotocol/registry/blob/main/docs/guides/consuming/use-rest-api.md
- **Test Commands**: [anthropic-api-test-commands.md](anthropic-api-test-commands.md)
- **Progress Notes**: [.scratchpad/anthropic-api-v0-jwt-auth-progress.md](../../.scratchpad/anthropic-api-v0-jwt-auth-progress.md)
No comments yet. Be the first to comment!