MCP gateway: OAuth-protected endpoints for remote and hosted MCP servers, plus a Grok API.
io.github.helv-io/skgate — MCP Gateway
The MCP gateway server provides OAuth-protected endpoints for remote and hosted MCP servers. It also includes a Grok API. The component is identified as “MCP gateway” and published under io.github.helv-io/skgate (slug: io-github-helv-io-skgate).
🛠️ Key Features
OAuth-protected endpoints
Gateway for remote MCP servers
Support for hosted MCP servers
Grok API
🚀 Use Cases
Accessing remote MCP servers through an OAuth-secured gateway
Routing requests to hosted MCP servers via gateway endpoints
Using the included Grok API alongside MCP access
⚡ Developer Benefits
Centralized gateway for OAuth-secured MCP connectivity
Combined availability of MCP gateway endpoints and a Grok API
⚠️ Limitations
Available capabilities are described only at the gateway and API level; no additional tools or configuration details are provided in the source data.
Use your Grok subscription as an OpenAI-compatible API, and serve your MCP servers from one OAuth-protected gateway.
Yes, all MCP servers: everyone's welcome. skgate can run them for you too, so no more stacks. It just works.
Name Origin
skgate /ɛsˈkɑːɡeɪt/ (ess-KAH-gate)
"sk" is what most AI API keys start with, or so I perceive it, and "gate" is for gateway. Bit rubbish as names go, but it's ours.
The plane in the logo is an inside joke. The public wouldn't understand it, and I'm not about to explain it. Sorry.
Quick start
Before you start: an OIDC provider with a confidential client for skgate (admin login is OIDC only). Just trying it on one machine? docs/quickstart.md runs skgate with a bundled provider and no accounts.
A Grok subscription is recommended but not required.
from openai import OpenAI
client = OpenAI() # reads the two variables above
r = client.chat.completions.create(model="grok-latest", messages=[{"role": "user", "content": "Say hi"}])
print(r.choices[0].message.content)
Model alias: one name that always points at the newest model
Map grok-latest to the latest available model. Change the target in this one place and every app using grok-latest is upgraded at once, with no client config changes.
Grok > Details > Model aliases: alias grok-latest, target the newest model in the list (for example grok-4.7), Save.
Needs the latest image and Grok signed in. The first time, Pick MCP helper model next to the button opens the model picker right on the page.
mcp upstreams > Add upstream > Type managed (package or repository):
MCP source URL / package, one of:
Source
Runs as
@modelcontextprotocol/server-everything
npm package, npx
pypi:mcp-server-time
PyPI package, uvx
https://github.com/example-org/notes-mcp
git repo: clone, install, run (private: Access token)
Suggest configuration. skgate fetches the README and manifests (package.json, pyproject.toml, server.json), the MCP helper model proposes command, args, install step and env names (marked secret or not, required or optional), and the Manual configuration fields are filled in with a confidence and any warnings. Nothing is saved yet. Point it at the repo, fill in the variables it needs, and it just works.
Set an alias, fill in the variables you need (empty ones are not passed to the server), Save. The server is at https://skgate.example.com/mcp/<alias>.
If the button is greyed out, hover it: sign in to Grok on status, or use Pick MCP helper model beside it.
MCP client: one upstream or all of them
URL
Serves
https://skgate.example.com/mcp/<alias>
One upstream; tool names unchanged
https://skgate.example.com/mcp
Every upstream marked In /mcp; tools prefixed <alias>-
Hosted connectors use OAuth (leave client ID and secret empty). Scripts and CLIs send a key:
On a RAM-constrained homelab, idle MCP servers should cost nothing. Managed servers (npx, uvx, git) are child processes of skgate. By default (Lifecycleon-demand) one starts on its first request and stops after 10 minutes without requests; the next request starts it again.
text
before 3 MCP servers = 3 containers, always running
after 1 skgate container; 0 server processes while idle, 1 per server in use
text
stopped --request--> starting --> running --10 min idle--> stopped
Default is on-demand; Lifecyclealways-on starts the server at boot instead.
The first request after a stop waits until the server answers initialize (up to 60 s).
A server with a request in flight is never stopped. Stopping is SIGTERM, then SIGKILL after 5 s.
Idle time is idleTimeoutSeconds in import JSON (default 600; not in the form):
The aggregated /mcp includes remote and always-on upstreams marked In /mcp. On-demand servers are left out, so /mcp never starts them. Point a client at /mcp/<alias> to use one.
Remote upstreams have no process; there is nothing to idle.
An admin Stop keeps a server stopped until Start or Restart.
Set under environment: (or env_file); placeholders in .env.example.
Variable
Default
Purpose
PUBLIC_URL
http://localhost:8080
Public origin, no trailing slash.
OIDC_ISSUER
Issuer URL, equal to the provider's discovery issuer.
OIDC_CLIENT_ID, OIDC_CLIENT_SECRET
Confidential client credentials.
OIDC_SCOPES
openid profile email groups
Requested scopes.
OIDC_REDIRECT_URL
PUBLIC_URL/admin/oidc/callback
Callback registered at the provider.
OIDC_ALLOWED_EMAILS, OIDC_ALLOWED_GROUPS
empty
Comma lists limiting who is admin.
MCP_OAUTH_REQUIRE_CONSENT
true
Approve/Deny page after login at /authorize.
SECRETS_KEY
random secrets.key file
Encrypts stored upstream secrets. 32-byte base64 or a passphrase.
GITHUB_TOKEN
empty
Optional GitHub token for Suggest when it reads a repository. An upstream's own access token takes precedence. Raises GitHub's rate limit.
LISTEN_ADDR
:8080
Listen address.
DB_PATH
/data/skgate.db
SQLite file.
LOG_LEVEL
info
info or debug.
TZ
UTC
Time zone for the UI and logs, for example America/New_York.
PUID, PGID
1000
Run-as ids; never 0.
MANAGED_DIR
/data/managed
Work dirs and clones of managed upstreams.
MANAGED_MAX_PROCS
0
Concurrent managed processes; 0 is unlimited.
Grok needs no variables; its base URL and aliases are in its Details dialog.
Everything lives in /data (skgate.db, secrets.key): back up both.
Reverse proxy
Set PUBLIC_URL to the public https origin. No forward-auth on /v1, /mcp, /authorize, /token, /register, /.well-known. Only Traefik is tested by the author; open an issue with feedback.
Traefik
yaml
services:skgate:container_name:skgateimage:ghcr.io/helv-io/skgate:latestrestart:alwaysnetwork_mode:web# existing Docker network shared with Traefik; no ports neededenvironment:-PUBLIC_URL=https://skgate.example.com# the public https origin-OIDC_ISSUER=https://auth.example.com-OIDC_CLIENT_ID=skgate-OIDC_CLIENT_SECRET=change-mevolumes:-./data:/datahealthcheck:test: ["CMD", "/skgate", "healthcheck"]
interval:30stimeout:5sretries:3labels:# no forward-auth middleware on /v1, /mcp, /authorize, /token, /register, /.well-known-traefik.enable=true-traefik.http.routers.skgate.rule=Host(`skgate.example.com`)-traefik.http.routers.skgate.entryPoints=websecure-traefik.http.services.skgate.loadbalancer.server.port=8080
skgate.example.com {
# Host and X-Forwarded-* are set by default; no body limit, no response timeout
reverse_proxy skgate:8080 {
flush_interval -1 # SSE streaming
}
}
HAProxy
haproxy
defaults
mode http
timeout connect 5s
timeout client 1h # long streams
timeout server 1h
timeout tunnel 1h
frontend https
bind :443 ssl crt /etc/haproxy/certs/skgate.pem alpn h2,http/1.1
option forwardfor # X-Forwarded-For; Host is kept, responses are not buffered
http-request set-header X-Forwarded-Proto https
default_backend skgate
backend skgate
option httpchk GET /healthz
server skgate skgate:8080 check
Apache
apache
# a2enmod ssl proxy proxy_http headers
<VirtualHost *:443>
ServerName skgate.example.com
SSLEngine on
SSLCertificateFile /etc/ssl/skgate/fullchain.pem
SSLCertificateKeyFile /etc/ssl/skgate/privkey.pem
# keep Host
ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto "https"
# long streams
ProxyTimeout 3600
# flushpackets: no buffering (SSE)
ProxyPass / http://skgate:8080/ flushpackets=on
ProxyPassReverse / http://skgate:8080/
</VirtualHost>
OIDC setup
Client setting
Value
Type
Confidential, client_secret_basic or client_secret_post
Flow
Authorization code with PKCE S256
Redirect URI
https://skgate.example.com/admin/oidc/callback
Scopes
openid profile email groups (if groups is rejected: OIDC_SCOPES=openid profile email)
Without OIDC_* the admin answers 503. Every user your provider lets in is an admin: restrict the provider, or set OIDC_ALLOWED_EMAILS / OIDC_ALLOWED_GROUPS. Hints for Authelia, Authentik, Keycloak, Zitadel and Pocket ID: docs/oidc.md.
Security notes
Virtual keys are stored as SHA-256 hashes; upstream credentials are AES-256-GCM encrypted.
/authorize needs an admin session.
Managed upstreams run admin-supplied commands; use the slim image to disable them.
A key can be allowed in the URL (?key=) for clients that cannot send headers. It is off per key by default, because URLs leak into logs, history and referrers.