@serpstat/oauth-dcr-cleaner
v1.0.0
Published
Middleware proxy for Ory Hydra Dynamic Client Registration to fix empty metadata fields validation issues with MCP clients like Claude Code
Downloads
82
Readme
OAuth DCR Cleaner Proxy
Middleware proxy for Ory Hydra Dynamic Client Registration (DCR) that removes empty metadata fields from responses to fix validation issues with MCP clients like Claude Code.
The Problem
Claude Code (v2.0.65+) and other mostly Anthropics MCP clients use strict validation for OAuth Dynamic Client Registration responses. When optional metadata fields like client_uri, logo_uri, or contacts are returned as empty strings or empty arrays or null, validation fails:
ZodError: [
{
"code": "invalid_format",
"format": "url",
"path": ["client_uri"],
"message": "Invalid URL"
}
]Root Cause:
- Claude Code doesn't send metadata fields during DCR registration
- Ory Hydra returns empty strings
""for unset optional fields - Claude Code validates these as invalid URLs and fails
Related issue: anthropics/claude-code#13685
The Solution
This proxy sits between the OAuth client and Ory Hydra, removing empty metadata fields from DCR responses before they reach the client:
Claude Code → DCR Proxy (Port 4446) → Ory Hydra (Port 4444)
↓
Removes empty fields
↓
Claude Code ← Clean Response ✅Features
- Removes empty strings, null values, and empty arrays
- Configurable list of fields to clean
- Proxies POST /oauth2/register (registration)
- Proxies GET /oauth2/register/:clientId (get client info)
- Pass-through for other OAuth endpoints
- Detailed logging with Winston
- Health check endpoint
- Docker support
- TypeScript with full type safety
Installation
Via npm
npm install -g @serpstat/oauth-dcr-cleanerFrom source
git clone https://github.com/SerpstatGlobal/oauth-dcr-cleaner.git
cd oauth-dcr-cleaner
npm install
npm run buildUsage
Production with PM2 (Recommended)
# Install dependencies and build
npm install
npm run build
# Start with PM2
pm2 start ecosystem.config.js
# View logs
pm2 logs oauth-dcr-cleaner
# Monitor
pm2 monit
# Restart
pm2 restart oauth-dcr-cleaner
# Stop
pm2 stop oauth-dcr-cleanerEdit ecosystem.config.js to configure:
env: {
NODE_ENV: 'production',
PROXY_PORT: 4446,
HYDRA_URL: 'http://127.0.0.1:4444',
FIELDS_TO_CLEAN: 'client_uri,logo_uri,tos_uri,policy_uri,contacts',
LOG_LEVEL: 'info',
}Command Line (Development)
# Set environment variables
export HYDRA_URL=http://127.0.0.1:4444
export PROXY_PORT=4446
# Start the proxy
npm run devDocker (Optional)
Build and run with Docker Compose:
# Clone the repository
git clone https://github.com/SerpstatGlobal/oauth-dcr-cleaner.git
cd oauth-dcr-cleaner
# Edit docker-compose.yml to configure your HYDRA_URL
# Build and start
docker-compose up -d --build
# View logs
docker-compose logs -f
# Stop
docker-compose downOr build Docker image manually:
# Build the image
docker build -t oauth-dcr-cleaner .
# Run the container
docker run -d \
-p 4446:4446 \
-e HYDRA_URL=http://host.docker.internal:4444 \
-e FIELDS_TO_CLEAN=client_uri,logo_uri,tos_uri,policy_uri,contacts \
-e LOG_LEVEL=info \
oauth-dcr-cleanerConfiguration
Environment Variables
| Variable | Default | Description |
|-------------------|---------------------------------------------------|---------------------------------------------------|
| PROXY_PORT | 4446 | Port for the proxy server |
| HYDRA_URL | http://127.0.0.1:4444 | Ory Hydra base URL |
| FIELDS_TO_CLEAN | client_uri,logo_uri,tos_uri,policy_uri,contacts | Comma-separated list of fields to remove if empty |
| LOG_LEVEL | info | Logging level (error, warn, info, debug) |
Ory Hydra Configuration
Update your OAuth discovery metadata to point to the proxy:
# Before
registration_endpoint: https://auth.example.com/oauth2/register
# After
registration_endpoint: https://auth.example.com:4446/oauth2/registerOr if using nginx:
location = /oauth2/register {
proxy_pass http://127.0.0.1:4446;
}
location ~ ^/oauth2/register/[^/]+$ {
proxy_pass http://127.0.0.1:4446;
}How It Works
Request Flow
- Client sends DCR request → Proxy
- Proxy forwards → Ory Hydra
- Ory Hydra responds with client data:
{ "client_id": "xxx", "client_uri": "", // Empty string "contacts": [], // Empty array "logo_uri": "" // Empty string } - Proxy cleans response by removing empty fields:
{ "client_id": "xxx" // Empty fields removed } - Client receives clean response → No validation errors ✅
Cleaning Logic
A field is removed if it's:
nullorundefined- Empty string
"" - Empty array
[]
API Endpoints
POST /oauth2/register
Proxies Dynamic Client Registration requests to Ory Hydra and cleans the response.
Request:
curl -X POST http://localhost:4446/oauth2/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My App",
"redirect_uris": ["https://app.example.com/callback"]
}'Response: Client metadata with empty fields removed
GET /oauth2/register/:clientId
Retrieves client registration data and cleans the response.
Request:
curl http://localhost:4446/oauth2/register/abc-123 \
-H "Authorization: Bearer YOUR_TOKEN"Response: Client metadata with empty fields removed
GET /health
Health check endpoint.
curl http://localhost:4446/healthResponse:
{
"status": "ok",
"service": "dcr-cleaner",
"version": "1.0.0",
"hydraUrl": "http://127.0.0.1:4444"
}Development
Prerequisites
- Node.js >= 18.0.0
- npm or yarn
Setup
# Install dependencies
npm install
# Run in development mode
npm run dev
# Build for production
npm run build
# Run tests (coming soon)
npm testProject Structure
oauth-dcr-cleaner/
├── src/
│ ├── index.ts # Express server & endpoints
│ ├── cleaner.ts # Field cleaning logic
│ ├── config.ts # Configuration loader
│ └── logger.ts # Winston logger setup
├── docs/ # Additional documentation
├── Dockerfile # Docker image
├── docker-compose.yml # Docker Compose config
├── package.json # npm package config
├── tsconfig.json # TypeScript config
└── README.md # This fileDeployment
Step-by-step Production Setup
1. Install on server:
cd /opt
git clone https://github.com/SerpstatGlobal/oauth-dcr-cleaner.git
cd oauth-dcr-cleaner
npm install
npm run build2. Configure (edit ecosystem.config.js):
env: {
NODE_ENV: 'production',
PROXY_PORT: 4446,
HYDRA_URL: 'http://127.0.0.1:4444', // Your Ory Hydra URL
FIELDS_TO_CLEAN: 'client_uri,logo_uri,tos_uri,policy_uri,contacts',
LOG_LEVEL: 'info',
}3. Start with PM2:
pm2 start ecosystem.config.js
pm2 save
pm2 startup # Enable auto-start on reboot4. Configure nginx to proxy DCR requests:
# Proxy DCR endpoints through the cleaner
location = /oauth2/register {
proxy_pass http://127.0.0.1:4446;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location ~ ^/oauth2/register/[^/]+$ {
proxy_pass http://127.0.0.1:4446;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Other OAuth endpoints go directly to Hydra
location /oauth2/ {
proxy_pass http://127.0.0.1:4444;
}5. Reload nginx:
nginx -t && systemctl reload nginx6. Test:
curl http://localhost:4446/healthTroubleshooting
Proxy not cleaning fields
Check configuration:
curl http://localhost:4446/healthEnable debug logging:
export LOG_LEVEL=debug
npm startOry Hydra connection refused
Check HYDRA_URL:
- For local development:
http://127.0.0.1:4444 - For Docker:
http://host.docker.internal:4444 - For production: Use internal Docker network or actual hostname
Claude Code still fails
Make sure your OAuth discovery points to the proxy:
curl https://your-server.com/.well-known/oauth-authorization-server | grep registration_endpoint
# Should return: https://your-server.com:4446/oauth2/registerContributing
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests
- Submit a pull request
License
MIT License - see LICENSE file for details
Related Projects
- Ory Hydra - OAuth 2.0 and OpenID Connect server
- Model Context Protocol - MCP specification
Support
- 🐛 Report a bug
- 💡 Request a feature
- 📧 Email: [email protected]
Authors
Made by Serpstat with ❤️ to fix Claude + Ory Hydra integration
