A full-featured Model Context Protocol (MCP) gateway with a web UI for registering and managing both local and remote MCP servers — including automatic OAuth 2.0 authentication for remote servers like Atlassian, GitHub, and others.
MCP Gateway acts as a centralized proxy and aggregator that connects to multiple MCP servers and exposes their tools, resources, and prompts through a single unified interface. It provides:
- Web UI for registering, configuring, and monitoring MCP servers
- Local server support via stdio transport (spawn child processes)
- Remote server support via SSE and Streamable HTTP transports
- Flexible authentication — supports multiple auth modes: OAuth 2.0 with auto-discovery, static bearer tokens, API keys, and custom headers
- Automatic OAuth 2.0 discovery — just provide the server URL (e.g.
https://mcp.atlassian.com/v1/mcp) and the gateway auto-discovers authorization endpoints via.well-known/oauth-authorization-serverand.well-known/oauth-protected-resource - PKCE + Dynamic Client Registration — supports both pre-registered client IDs and RFC 7591 dynamic registration out of the box
- Bearer token support — for APIs like GitHub Copilot MCP that require pre-authenticated tokens
- Live status updates via Server-Sent Events (SSE)
- Auto-reconnection with exponential backoff
- Persistent configuration stored as JSON on disk
┌─────────────────────────────────────────────────────────┐
│ MCP Gateway │
│ │
│ ┌──────────┐ ┌──────────┐ ┌────────────────────┐ │
│ │ React UI │ │ REST API │ │ OAuth Handler │ │
│ │ (Vite) │◄►│ (Express)│◄►│ (Auth Code + PKCE) │ │
│ └──────────┘ └────┬─────┘ └────────────────────┘ │
│ │ │
│ ┌──────┴──────┐ │
│ │ Gateway │ │
│ │ Engine │ │
│ └──────┬──────┘ │
│ │ │
│ ┌────────────────┼─────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────┐ ┌──────────┐ ┌──────────────┐ │
│ │stdio │ │ SSE │ │ Streamable │ │
│ │Client│ │ Client │ │ HTTP Client │ │
│ └──┬───┘ └────┬─────┘ └──────┬───────┘ │
└────┼──────────────┼──────────────────┼──────────────────┘
│ │ │
▼ ▼ ▼
┌─────────┐ ┌───────────┐ ┌───────────────┐
│ Local │ │ Remote │ │ Remote │
│ MCP │ │ MCP │ │ MCP │
│ Server │ │ Server │ │ Server │
│ (stdio) │ │ (SSE) │ │ (HTTP) │
└─────────┘ │ + OAuth │ │ + OAuth │
└───────────┘ └───────────────┘
| Component | Path | Description |
|---|---|---|
| Server Entry | src/server/index.ts |
Express app setup, startup, and graceful shutdown |
| Gateway Engine | src/server/gateway.ts |
Core logic: connects to MCP servers, discovers capabilities, manages lifecycle |
| API Routes | src/server/api.ts |
REST endpoints for CRUD, connection control, tool invocation, and SSE events |
| OAuth Manager | src/server/oauth.ts |
OAuthClientProvider implementation backed by the MCP SDK's native auth flow with auto-discovery |
| Store | src/server/store.ts |
JSON file-based persistence for server configs and OAuth state |
| Types | src/server/types.ts |
Shared TypeScript type definitions |
| React App | src/client/App.tsx |
Main UI component with server list, stats, and modal management |
| Server Card | src/client/components/ServerCard.tsx |
Individual server display with status, capabilities, and actions |
| Add Modal | src/client/components/AddServerModal.tsx |
Form for registering new local or remote servers |
| Edit Modal | src/client/components/EditServerModal.tsx |
Form for updating existing server configurations |
| API Client | src/client/api.ts |
Typed fetch wrappers for all API endpoints |
- Node.js >= 18.x
- npm >= 9.x
# Clone the repository
git clone <your-repo-url> mcp-gateway
cd mcp-gateway
# Install dependencies
npm installRun both the API server and the Vite dev server concurrently:
npm run devThis starts:
- API Server on
http://localhost:3099 - UI Dev Server on
http://localhost:5173(with proxy to API)
Open http://localhost:5173 in your browser.
# Build both client and server
npm run build
# Start the production server (serves the UI as static files)
npm startThe production server runs on port 3099 by default and serves the UI from the built static files.
| Variable | Default | Description |
|---|---|---|
PORT |
3099 |
Port for the API/HTTP server |
HOST |
0.0.0.0 |
Host to bind the server to |
GATEWAY_BASE_URL |
http://localhost:3099 |
Public URL of the gateway (used for OAuth callbacks) |
DATA_DIR |
./data |
Directory for persistent storage |
Server configurations and OAuth tokens are stored in data/gateway-store.json. This file is automatically created on first run. You can back up or version-control this file as needed.
Security Note: OAuth tokens (including refresh tokens) are stored in this file. Ensure appropriate file permissions in production environments.
- Click "Add Server" in the top-right
- Select "Local Server"
- Fill in:
- Name: A friendly name (e.g., "Filesystem Server")
- Command: The executable (e.g.,
npx) - Arguments: Command arguments (e.g.,
-y @modelcontextprotocol/server-filesystem /tmp) - Working Directory (optional): Where to run the command
- Environment Variables (optional): Key-value pairs
- Click "Add Server"
The gateway will spawn the process and connect via stdio.
- Click "Add Server"
- Select "Remote Server"
- Choose the transport protocol:
- SSE (Server-Sent Events) — for servers using the SSE transport
- Streamable HTTP — for servers using the newer Streamable HTTP transport
- Fill in:
- Name: A friendly name
- URL: The server's endpoint URL
- Custom Headers (optional): Additional HTTP headers
- Click "Add Server"
The gateway supports multiple authentication modes for remote MCP servers:
| Mode | Use Case | Example |
|---|---|---|
| None | Public servers with no authentication | Development/testing servers |
| OAuth | Servers implementing MCP OAuth 2.0 spec | Atlassian, compliant MCP servers |
| Bearer | Pre-authenticated bearer tokens | GitHub Copilot MCP, custom APIs |
| API Key | API key in a custom header | Third-party services |
| Custom | Arbitrary authentication headers | Legacy or custom auth schemes |
For remote MCP servers that require OAuth 2.0 authentication (like https://mcp.atlassian.com/v1/mcp):
- Follow the steps above for adding a remote server
- Enter the server URL (e.g.
https://mcp.atlassian.com/v1/mcp) - In the Authentication section, select OAuth
- Optionally fill in:
- Client ID — required if the server needs a pre-registered OAuth app; leave blank to attempt RFC 7591 Dynamic Client Registration automatically
- Client Secret (optional) — only needed for confidential clients; public clients omit this
- Scopes (optional) — if omitted, the server's default scopes are used
- Click "Add Server"
That's it. You do not need to manually enter authorization URLs, token URLs, or any other OAuth endpoint details. The gateway automatically discovers them by fetching the server's .well-known/oauth-authorization-server and .well-known/oauth-protected-resource metadata — exactly as the MCP specification requires.
When you enable the server or click Authenticate, the gateway:
- Discovers the OAuth authorization server metadata from
.well-knownendpoints - Attempts Dynamic Client Registration if no Client ID was provided
- Generates a PKCE code challenge and redirects you to the authorization page
- Exchanges the authorization code for tokens on callback
- Stores tokens securely and automatically refreshes them when they expire
You can revoke tokens at any time from the server's detail panel.
For APIs that require a pre-authenticated bearer token (like https://api.githubcopilot.com/mcp/):
- Follow the steps above for adding a remote server
- Enter the server URL (e.g.
https://api.githubcopilot.com/mcp/) - In the Authentication section, select Bearer
- Enter your access token in the Bearer Token field
- Click "Add Server"
The gateway will include the token as Authorization: Bearer <token> with every request.
Note: GitHub Copilot's MCP endpoint does not implement the standard OAuth discovery endpoints (
.well-known/oauth-authorization-server), so OAuth auto-discovery won't work. Use the Bearer token mode instead with a valid GitHub Copilot access token.
For services that use API key authentication:
- Follow the steps above for adding a remote server
- In the Authentication section, select API Key
- Fill in:
- API Key — your API key value
- Header Name (optional) — defaults to
X-API-Key - Value Prefix (optional) — e.g.,
ApiKeyto sendApiKey your-key
- Click "Add Server"
For custom authentication schemes:
- Follow the steps above for adding a remote server
- In the Authentication section, select Custom
- Add one or more authentication headers (key-value pairs)
- Click "Add Server"
This allows you to configure arbitrary headers for authentication, such as custom tokens, signatures, or multi-header auth schemes.
Each server card in the UI provides:
- Status indicator: Connected (green), Connecting (yellow), Error (red), Disconnected (gray), Awaiting OAuth (blue)
- Enable/Disable toggle: Control whether a server should be active
- Connect/Disconnect/Reconnect: Manual connection control
- Edit: Modify server configuration
- Delete: Remove the server entirely
- Expand: View detailed configuration, OAuth status, and discovered capabilities (tools, resources, prompts)
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/servers |
List all servers with status |
GET |
/api/servers/:id |
Get a specific server |
POST |
/api/servers |
Register a new server |
PATCH |
/api/servers/:id |
Update server configuration |
DELETE |
/api/servers/:id |
Remove a server |
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/servers/:id/connect |
Connect to a server |
POST |
/api/servers/:id/disconnect |
Disconnect from a server |
POST |
/api/servers/:id/reconnect |
Reconnect to a server |
POST |
/api/servers/:id/refresh |
Refresh discovered capabilities |
POST |
/api/servers/:id/enable |
Enable a server |
POST |
/api/servers/:id/disable |
Disable a server |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/servers/:id/auth/status |
Check OAuth status |
POST |
/api/servers/:id/auth/initiate |
Start OAuth flow (auto-discovers endpoints, returns auth URL) |
POST |
/api/servers/:id/auth/revoke |
Revoke OAuth tokens and clear stored state |
GET |
/oauth/callback/:serverId |
Per-server OAuth redirect callback (handled automatically) |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/tools |
List all tools across connected servers |
GET |
/api/resources |
List all resources across connected servers |
GET |
/api/prompts |
List all prompts across connected servers |
POST |
/api/tools/call |
Invoke a tool (auto-routes or specify server) |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/events |
SSE stream of gateway events |
GET |
/api/health |
Health check with server stats |
# Local server
curl -X POST http://localhost:3099/api/servers \
-H "Content-Type: application/json" \
-d '{
"name": "Filesystem Server",
"transport": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
"enabled": true
}'
# Remote server with OAuth (auto-discovery — no manual URLs needed!)
curl -X POST http://localhost:3099/api/servers \
-H "Content-Type: application/json" \
-d '{
"name": "Atlassian MCP",
"transport": "streamable-http",
"url": "https://mcp.atlassian.com/v1/mcp",
"auth": {
"mode": "oauth"
},
"enabled": true
}'
# Remote server with a pre-registered OAuth client ID
curl -X POST http://localhost:3099/api/servers \
-H "Content-Type: application/json" \
-d '{
"name": "My Remote MCP",
"transport": "sse",
"url": "https://mcp.example.com/sse",
"auth": {
"mode": "oauth",
"clientId": "my-client-id",
"clientSecret": "my-client-secret",
"scopes": ["read", "write"]
},
"enabled": true
}'
# Remote server with bearer token (e.g. GitHub Copilot)
curl -X POST http://localhost:3099/api/servers \
-H "Content-Type: application/json" \
-d '{
"name": "GitHub Copilot MCP",
"transport": "streamable-http",
"url": "https://api.githubcopilot.com/mcp/",
"auth": {
"mode": "bearer",
"token": "your-access-token-here"
},
"enabled": true
}'
# Remote server with API key
curl -X POST http://localhost:3099/api/servers \
-H "Content-Type: application/json" \
-d '{
"name": "My API Service",
"transport": "sse",
"url": "https://api.example.com/mcp",
"auth": {
"mode": "api-key",
"key": "your-api-key",
"headerName": "X-API-Key"
},
"enabled": true
}'
# Remote server with custom auth headers
curl -X POST http://localhost:3099/api/servers \
-H "Content-Type: application/json" \
-d '{
"name": "Custom Auth Server",
"transport": "streamable-http",
"url": "https://custom.example.com/mcp",
"auth": {
"mode": "custom",
"headers": {
"X-Custom-Token": "token-value",
"X-Tenant-ID": "my-tenant"
}
},
"enabled": true
}'- Backend: Node.js, Express, TypeScript
- Frontend: React 19, Vite 6, Tailwind CSS 3, Lucide Icons
- MCP SDK:
@modelcontextprotocol/sdkv1.12+ (nativeOAuthClientProviderinterface) - OAuth: Authorization Code flow with PKCE (RFC 7636), automatic server metadata discovery (RFC 8414 / RFC 9728), optional Dynamic Client Registration (RFC 7591)
- Storage: JSON file-based persistence
- Live Updates: Server-Sent Events (SSE)
When you enable OAuth for a remote server, the MCP SDK's transport layer handles the entire flow:
- 401 Detection — The transport attempts to connect. If the server returns
401 Unauthorized, the auth flow begins. - Protected Resource Metadata — The SDK fetches
/.well-known/oauth-protected-resourcefrom the server to find the authorization server URL. - Authorization Server Metadata — The SDK fetches
/.well-known/oauth-authorization-server(RFC 8414) or falls back to OpenID Connect Discovery to learn theauthorization_endpoint,token_endpoint,registration_endpoint, supported scopes, PKCE methods, etc. - Dynamic Client Registration — If no
clientIdwas provided, the SDK attempts RFC 7591 registration at the discoveredregistration_endpoint. - PKCE Authorization — A code verifier/challenge pair is generated, and the user is redirected to the authorization endpoint.
- Token Exchange — After consent, the callback at
/oauth/callback/{serverId}exchanges the authorization code for tokens. - Automatic Refresh — On subsequent connections, expired tokens are refreshed transparently using the stored refresh token.
mcp-gateway/
├── src/
│ ├── server/ # Backend (Express + MCP SDK)
│ │ ├── index.ts # Server entry point
│ │ ├── gateway.ts # Core gateway engine
│ │ ├── api.ts # REST API routes
│ │ ├── oauth.ts # OAuth 2.0 handler
│ │ ├── store.ts # JSON file persistence
│ │ └── types.ts # Shared type definitions
│ └── client/ # Frontend (React + Vite)
│ ├── index.html # HTML entry
│ ├── main.tsx # React entry
│ ├── index.css # Tailwind + custom styles
│ ├── App.tsx # Main application component
│ ├── api.ts # API client
│ └── components/
│ ├── ServerCard.tsx # Server display card
│ ├── AddServerModal.tsx # Add server form
│ ├── EditServerModal.tsx # Edit server form
│ └── OAuthNotification.tsx # OAuth callback notification
├── data/ # Persistent storage (auto-created)
│ └── gateway-store.json # Server configs + OAuth state (tokens, client info, PKCE verifiers)
├── package.json
├── tsconfig.json
├── tsconfig.server.json
├── vite.config.ts
├── tailwind.config.js
├── postcss.config.js
└── README.md
- OAuth state (tokens, client info, PKCE verifiers) is stored on disk in
data/gateway-store.json. Restrict file permissions in production. - Client secrets are masked in API responses (shown as
••••••••). - PKCE (Proof Key for Code Exchange) is used for all OAuth flows to prevent authorization code interception.
- Per-server callback URLs (
/oauth/callback/{serverId}) ensure callbacks are routed to the correct provider without shared state. - OAuth metadata is always fetched from the server's
.well-knownendpoints — no manual endpoint URLs are trusted from user input. - The gateway's
OAuthClientProviderimplementation supports the SDK'sinvalidateCredentials()callback to automatically clear stale tokens or client registrations. - The gateway does not expose MCP server credentials through the UI or API responses.
MIT