feat(kong-mcp): add MCP OAuth gateway plugin for Kong#35
Conversation
- Add a Kong go-pdk plugin that fronts MCP servers with AuthGate OAuth - Advertise the auth entry point via 401 challenge and RFC 9728 metadata - Validate access tokens offline with RS256 plus JWKS and pin RS algorithms - Forward subject and scope to MCP upstreams after verification - Provide declarative Kong config, Dockerfile, and compose demo stack - Document architecture, handshake, and validation matrix in EN and zh-TW
appleboy
left a comment
There was a problem hiding this comment.
本地跑了 simplify(reuse / simplification / efficiency / altitude 四個面向)+ security review 的結果,逐項列在 inline comments(1 個 High 安全性問題 + 10 個品質/效能建議)。
另外幾項有討論過、但建議不在本 PR 處理的:
- 改用 sdk-go 的
jwksauth取代 keyfunc/golang-jwt — 屬於設計決定(commit message 明確選了 keyfunc),且 SDK 目前沒有 leeway 選項;若維持 keyfunc,建議在 README 註明原因(如同 go-oidc 標示 "no SDK")。 require_audience預設改true— root cause 在 AuthGate 還沒發 per-resourceaud,等 AuthGate 支援後再翻轉預設值。- kong.yml 用 YAML anchor 去重共用欄位 — 兩個 service 的 demo 直接複寫比較清楚,但服務數量多了之後值得做。
- PRM 集中成單一 dedicated route(取代每條 route 都要多掛一條 well-known path 的「Routing gotcha」)— 架構性重構,可開後續 issue。
- Clear inbound X-MCP-Subject and X-MCP-Scope before setting them so a client holding a sub-less token cannot smuggle forged identity headers - Serve Protected Resource Metadata on exact path match only so a prefix route can never answer for another resource's metadata - Reject non-Bearer Authorization schemes instead of parsing the raw header value as a token - Encode error bodies with json.Marshal and add the RFC 6750 insufficient_scope challenge header on 403 - Remove the unused hmac_secret HS256 fallback and prm_path_prefix knob; accepted algorithms are now always pinned to the RS family - Build the JWT parser once per plugin instance and switch the JWKS cache to a read-write lock so the hot path avoids the global mutex - Drop the no-op chmod and user switching in the Dockerfile - Sync the EN and zh-TW configuration docs with the removed knobs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add an explicit USER kong to the final stage to satisfy the DS-0002 scanner, which checks the Dockerfile itself rather than the base image default Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
appleboy
left a comment
There was a problem hiding this comment.
第二輪 max-effort review(9 個 finder 角度 + 來源驗證 + gap sweep,含 RFC 9728/6750 spec 與 go-pdk / golang-jwt / jwkset / keyfunc 原始碼比對)的結果。15 項 findings 列在 inline comments,修正已隨後續 commit 推上(plugin 版本 bump 到 0.3.0)。
這輪的行為變更摘要:
- 驗證基礎設施故障(JWKS 抓不到、設定缺欄位)改回
503/500,不再偽裝成401 invalid_token誘使 client 重跑 OAuth - JWKS client 改為 fail-fast 組裝(不再用
keyfunc.NewDefault):第一次抓取失敗會回錯誤並於下個請求重試,不會快取空 key set;HTTP / unknown-kid 等待上限 10s - PRM
resource固定為 canonical URL(RFC 9728 §3.3),audience設定只影響 token 驗證
檢查過但刻意不改的: array 形式的 scope claim(AuthGate 發 string、與 sibling examples 語義一致)、拒絕無 sub 的 token(會弄壞 M2M)、SetHeaders 合併 RPC(空值語義不等價)、jwksCache 不淘汰(有界)、testissuer 整進 compose(建議開 follow-up issue)。
驗證後駁回的(不修): PRM 缺 resource_signing_alg_values_supported(該欄位是給 signed metadata 用的,不是 token alg 宣告)、401/403 加 Cache-Control(非預設可快取狀態碼)、challenge closure / keyFunc method value 的配置疑慮(escape analysis 證實 inlined / stack)。
- Serve the PRM resource as the canonical gateway_origin+resource_path per RFC 9728 §3.3 instead of the audience override - Build the JWKS client with fail-fast options: a failed first fetch is returned as an error and retried instead of being cached as an empty key set, with HTTP and unknown-kid waits capped at 10s - Answer 503 temporarily_unavailable for verification-infrastructure failures instead of 401 invalid_token, and log token rejection details instead of leaking them to unauthenticated clients - Validate the four required config fields on first request so an empty issuer can no longer silently disable iss validation - Add error="invalid_token" to the 401 challenge, restrict the PRM endpoint to GET/HEAD, tolerate a trailing slash, and omit scopes_supported when empty instead of emitting null - Unify error bodies on error/error_description and precompute the parser and per-instance strings in a setup once-block - Bind the demo admin API to loopback, add depends_on for the stub upstreams, make strip_path explicit, and add a .dockerignore - Document the audience semantics, required-field validation, demo matrix limits, JWKS failure mode, and CORS preflight caveat - Bump plugin version to 0.3.0 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Probe Kong liveness with kong health to satisfy the DS-0026 scanner and surface a dead pluginserver in docker compose ps Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@codex please review |
…zation - Reject requests carrying multiple Authorization headers with 400 so an unvalidated second credential can never be proxied past the gateway - Return 500 instead of a 401 challenge when request headers cannot be read - Return 500 when clearing or setting the trusted X-MCP-Subject/X-MCP-Scope headers fails, instead of forwarding client-supplied values upstream - Log the error and exit non-zero when the plugin server fails to start - Document that kong-mcp is configured via kong.yml, not environment variables or .env Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
appleboy
left a comment
There was a problem hiding this comment.
第三輪 lean sweep(PDK 錯誤路徑 + README/相依/設定/翻譯一致性)的結果。4 項 findings 列在 inline comments,修正已隨 6969c58 推上。
掃過但乾淨、不需修改的項目:
- 相依升級:jwkset v0.11.0 + keyfunc/v3 v3.8.0 已驗證為 drop-in(用到的 API 簽名完全相同,go.mod + tidy 即可,未在本 PR 套用);現用 jwkset v0.8.0 未被上游 retract。
go mod verify/go build/go vet/go mod tidy -diff全數通過。 - plugin
Priority = 1000:cors(2000)先跑(preflight 在進 plugin 前 short-circuit,README 的 CORS 建議在順序上成立),acl(950)/ rate-limiting(910)後跑,符合 Kong 自家「先認證再限流」慣例。 - zh-TW 第三輪段落(audience 列、required-fields 註記、矩陣前言、JWKS-HA、CORS):翻譯忠實、無語意偏移。
exitJSON的 Content-Type 處理、.gitignore的 build 輸出路徑、kong.yml_format_version: "3.0"、compose 的 proxy listen 設定皆無問題。
| } | ||
| if len(auths) > 1 { | ||
| challenge(400, conf.bearerMeta+`, error="invalid_request"`, | ||
| "invalid_request", "multiple Authorization headers are not allowed") |
There was a problem hiding this comment.
🔴 [Security / High] 重複 Authorization header:只驗第一個、全部轉發
GetHeader 只回傳第一個出現值(Kong PDK 文件行為),驗證通過後請求卻帶著所有 Authorization 出現值原樣轉發。攻擊者持任一有效 token 即可夾帶第二個 Authorization: token <PAT> 給讀取另一個出現值的後端——正好打穿「後端不再接受手寫 PAT」的目標。
修法(本 commit):改用 GetHeaders(1000)(帶 PDK 上限、避免截斷漏看)不分大小寫收集所有出現值,超過一個回 400 invalid_request + RFC 6750 格式的 WWW-Authenticate challenge。
順帶修掉同一行的次要問題:原本 auth, _ := GetHeader(...) 把 PDK 錯誤與「沒帶 token」混為一談,暫時性 RPC 錯誤會以 401 challenge 逼 client 重跑整個 OAuth flow;現在讀 header 失敗回 500(gateway 故障,可重試)。
| err = kong.ServiceRequest.SetHeader(h.name, h.value) | ||
| } | ||
| if err != nil { | ||
| _ = kong.Log.Err("failed to set trusted header ", h.name, ": ", err.Error()) |
There was a problem hiding this comment.
🔴 [Security / High] 受信任 header 清除失敗原本 fail open
四個 ClearHeader/SetHeader 的錯誤原本全以 _ = 丟棄:pluginserver respawn、bridge RPC 抖動時 ClearHeader("X-MCP-Subject") 失敗,Access 照常結束,Kong 把 client 自帶的 X-MCP-Subject/X-MCP-Scope 原樣轉發給被告知「無條件信任這兩個 header」的後端——round 1 修掉的 trust-header smuggling 從這條錯誤路徑繞回來了。同函式裡 GetPath 失敗已經回 500,這裡比照辦理。
修法(本 commit):clear/set 改為逐一檢查,任何失敗記 log 並回 500,不放行請求。
| // errors without logging, and a silent exit 0 reads as a healthy pluginserver. | ||
| func main() { | ||
| if err := server.StartServer(New, Version, Priority); err != nil { | ||
| slog.Error("plugin server exited", "error", err) |
There was a problem hiding this comment.
[Reliability] StartServer 失敗原本靜默 exit 0
go-pdk 的 StartServer 對 socket 開啟失敗只回傳 error、不印任何訊息(pbserver.go:191-194);_ = 丟棄後,prefix 目錄不可寫或 socket 衝突 → 零輸出、exit code 0,Kong supervisor 對著一個「看起來成功」的程序無限重生,手動 smoke-test 也會誤判正常——和先前 HEALTHCHECK 不涵蓋 pluginserver 的缺口疊加。
修法(本 commit):slog.Error 印出錯誤後 os.Exit(1)。
| ## Environment Setup | ||
|
|
||
| All examples require `AUTHGATE_URL` and `CLIENT_ID`. M2M examples additionally require `CLIENT_SECRET`. | ||
| All examples except [kong-mcp](kong-mcp/) require `AUTHGATE_URL` and `CLIENT_ID`. M2M examples additionally require `CLIENT_SECRET`. The kong-mcp gateway reads no environment variables — configure it via the plugin block in [`kong-mcp/kong.yml`](kong-mcp/kong.yml) (`issuer`, `gateway_origin`, `jwks_uri`, ...). |
There was a problem hiding this comment.
[Docs] 「All examples」的環境變數說明對 kong-mcp 不成立
kong-mcp 不讀任何環境變數、也不載入 .env——設定全在 kong.yml 的 plugin block(issuer、gateway_origin、jwks_uri...)。使用者照本節 export AUTHGATE_URL/CLIENT_ID 或寫 kong-mcp/.env 完全不會生效,且沒有任何訊息把他導向 kong.yml。
修法(本 commit):本行與下方 .env 載入說明各加上 kong-mcp 例外。
- Add a step-by-step macOS guide covering build, test issuer, demo stack, and the full validation matrix with expected outputs - Add a local-build compose file that mounts a cross-compiled binary into kong:3.9, bypassing in-Docker builds blocked by TLS-intercepting proxies - Add a local declarative config wiring the plugin to the bundled test issuer so the handshake runs end-to-end without a real AuthGate - Ignore the cross-compiled and default-name plugin binaries Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add a declarative config and compose file wired to a real AuthGate on the host, keeping the localhost issuer but fetching JWKS via host.docker.internal - Add a hands-on section covering discovery-driven config, the scope mismatch pitfall, stack switching, a no-credentials JWKS-connectivity check, and obtaining a real token via get-token.sh - Add troubleshooting rows for the real-AuthGate flow Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Reject tokens whose type claim is not access so leaked refresh tokens fail - Reject tokens carrying control chars in sub or scope before forwarding - Validate resource_path, gateway_origin, and leeway_seconds shape at setup - Serialize JWKS construction under a per-URI lock so a slow issuer cannot stall the gateway - Accept HEAD alongside GET on the protected-resource metadata path - Bind the unauthenticated admin API to container loopback in the demo - Build the plugin binary with trimpath and stripped symbols - Document cross-resource replay, token forwarding, and JWKS failure behavior
- Validate issuer, gateway_origin, and jwks_uri are absolute http(s) URLs and reject a trailing slash on resource_path - Bind the unauthenticated admin API to container loopback in the local and AuthGate compose variants - Mint a type claim from the test issuer, defaulting to access, so the hands-on flow passes and refresh-token rejection can be tested - Pin the built binary to the target architecture via TARGETOS and TARGETARCH - Correct the package doc to reflect that aud and scope checks are conditional - Fix hands-on working directories, container network derivation, Go version requirement, and troubleshooting commands
Code-review round 4 — applied + deferred (
|
| Severity | Area | Fix |
|---|---|---|
| 🔴 High | Token type | Reject non-access tokens (refresh tokens share key/iss/aud/scope); test issuer now mints type (default access, ?type=refresh for rejection tests) so the hands-on flow passes |
| 🔴 High | Demo security | KONG_ADMIN_LISTEN re-bound from 0.0.0.0:8001 to container loopback in the local / authgate compose variants (unauthenticated POST /config was reachable by sibling containers) |
| 🔴 High | Docs accuracy | Package godoc corrected — aud / scope checks are conditional, not always-on |
| 🟡 Med | Config validation | issuer / gateway_origin / jwks_uri must be absolute http(s) URLs; resource_path trailing slash rejected (silently broke PRM exact-match) |
| 🟡 Med | Hands-on | Fixed cd working dirs, container-network derivation (Compose v1 + v2), Go version (1.25.10), and the plugin-info/arch troubleshooting commands |
| 🟢 Low | Build | Binary pinned to target arch via TARGETOS / TARGETARCH |
Deferred (verified real, intentionally not fixed here)
- JWKS cold-start serialization (med): a black-holed
jwks_uriserializes
concurrent cold callers (each runs its own ~10s fetch). Fix: single-flight +
short negative-cache. Not applied — touches carefully-tuned concurrency code
with no test coverage. jwks_urichange leak (med): changingjwks_urion a live pluginserver
leaks the old keyfunc + its refresh goroutine (insert-only cache). Needs URI
lifecycle / eviction.jwksetnon-atomic refresh (med, plausible): one malformed key mid-rotation
can drop cached valid keys for up to an hour. Needs a dependency bump +
behavior verification.GetHeaders(1000)cap (med, plausible): defense-in-depth only — nginx
already rejects duplicateAuthorizationbefore the plugin runs.audienceoverride mismatch (low, plausible): already documented as a
README footgun.- Compose triplication (med, reuse): collapsing the three near-identical
compose files would break the documented HANDS-ON commands; left as-is.
Verification after the fixes: gofmt clean, go build + go vet pass for both
kong-mcp and go-jwks-multi.
🤖 Generated with Claude Code
Summary
Adds a new
kong-mcp/example: a Kong go-pdkplugin that puts a single OAuth front door in front of any number of MCP servers,
backed by AuthGate. Kong does not run the OAuth flow — it advertises the
entry point (401 challenge + RFC 9728 Protected Resource Metadata) and validates
the returned access token offline with RS256 + JWKS, then forwards it to the
MCP backend. The MCP client runs Auth Code + PKCE against AuthGate itself.
Architecture / flow
sequenceDiagram participant C as MCP client participant K as Kong + mcp-authgate participant A as AuthGate participant M as MCP server C->>K: GET /mcp/gitea (no token) K-->>C: 401 + WWW-Authenticate: resource_metadata C->>K: GET /.well-known/oauth-protected-resource/mcp/gitea K-->>C: 200 Protected Resource Metadata C->>A: Auth Code + PKCE (/authorize, /token) A-->>C: RS256 access token K-)A: fetch JWKS (cached / auto-rotated) C->>K: GET /mcp/gitea + Bearer jwt Note over K: verify sig(JWKS) + iss + exp + type=access (+ scope, + aud when require_audience) K->>M: forward + X-MCP-Subject / X-MCP-Scope M-->>K: 200 K-->>C: 200Review fixes — round 1 (
5c25762,af125ba)From the first simplify + security pass
(review):
X-MCP-Subject/X-MCP-Scopecleared beforebeing conditionally set (trust-header smuggling via sub-less tokens).
Bearerschemes rejected; JSONerror bodies via
json.Marshal; RFC 6750 challenge on 403.hmac_secretHS256 fallback andprm_path_prefixknob; algs always pinned to RS256/384/512.RWMutex.chmod, explicit non-rootUSER kong.Review fixes — round 2 (
f9ccd7c)From a max-effort review (9 finder angles, source-level verification against
go-pdk / golang-jwt / jwkset / keyfunc, RFC 9728 / 6750 spec checks, gap sweep —
review):
resourceis always the canonicalgateway_origin + resource_path; theaudienceknob now affects tokenaudvalidation only (the override previously broke discovery with the official
MCP TS SDK).
keyfunc.NewDefaultwith an explicitlyconfigured jwkset client — a failed first fetch returns an error and is
retried (no more silently cached empty key set), HTTP / unknown-kid waits
capped at 10s instead of 60s under the global lock.
missing config) now answer
503/500instead of401 invalid_token, soclients don't re-run OAuth on a gateway-side outage; token-rejection details
go to Kong's log instead of leaking JWKS URLs / dial errors to
unauthenticated callers; 401 challenge carries
error="invalid_token"(RFC 6750 §3.1); bodies unified on
error/error_description.request (
500+ critical log) — an emptyissuerpreviously disabled issvalidation silently (golang-jwt skips
WithIssuer("")), and go-pdk schemascannot mark fields required.
scopes_supportedomitted when empty instead ofnull.0.0.0.0:8001,unauthenticated
POST /config); explicitstrip_path: truewith a comment;depends_onfor the stub upstreams;.dockerignoreso config/doc editsdon't recompile the plugin.
which validation-matrix rows are runnable against the stub demo, the new
JWKS failure mode, and the CORS-preflight caveat for browser-based clients.
Deliberately not done (recorded in the reviews): switching to sdk-go's
jwksauth; flippingrequire_audiencedefault (blocked on AuthGate emittingper-resource
aud); array-formscopeclaims; rejecting sub-less tokens(breaks M2M); kong.yml YAML anchors; dedicated PRM route; wiring
go-jwks-multi/testissuerinto the compose demo (suggested follow-up issue).Review fixes — round 3 (
6969c58)From a lean sweep over the PDK error paths and repo-level consistency
(review):
Authorizationheaderare rejected with
400 invalid_request— previously only the first valuewas validated (
GetHeadersemantics) while every occurrence was forwarded,letting a client smuggle an unvalidated second credential (e.g. a legacy
PAT) past the gateway.
X-MCP-Subject/X-MCP-Scopeheaders now fails closed (500) — a PDK error previously fellthrough to proxying with client-supplied values intact, reopening the
round-1 trust-header smuggling via the error path.
500instead of a bare 401 challenge, so a transient RPC error no longer tells
the client to re-run the whole OAuth flow.
server.StartServererrors are logged and exit non-zero —previously a socket failure exited
0with no output, leaving Kongrespawn-looping a "healthy" pluginserver.
no environment variables /
.env; configuration lives inkong.yml.Checked clean in the same sweep (no changes needed): jwkset v0.11.0 +
keyfunc/v3 v3.8.0 verified as a drop-in bump (not applied here); plugin
priority 1000 ordering vs cors/acl/rate-limiting; zh-TW translation fidelity;
exitJSONContent-Type handling,.gitignore,kong.ymlformat version,compose listen config.
Hands-on guide + real-AuthGate path (
ecaed4a,3ee2f57)Added
kong-mcp/HANDS-ON.zh-TW.md: a macOS, end-to-end walkthrough that runs thefull MCP OAuth handshake on a laptop using the bundled
go-jwks-multi/testissueras a stand-in AuthGate — no real AuthGate needed — verifying each security
property row by row, then shows how to point the same stack at a real AuthGate.
Ships matching configs:
kong.local.yml+docker-compose.local.yml: plugin wired to the local testissuer; cross-compiles the plugin on the host and mounts it into stock
kong:3.9(works behind a TLS-intercepting corporate proxy where the in-Dockerbuild can't fetch modules).
kong.authgate.yml+docker-compose.authgate.yml: same stack pointed at areal AuthGate on the host, with the
localhostvshost.docker.internalgotcha pre-solved.
Review fixes — round 4 (
8459a87,668eede)From a max-effort review of the full PR diff (9 finder angles, per-finding
verification, gap sweep):
type=accesstokens are accepted. AuthGate signsrefresh tokens with the same key / iss / aud / scope — differing only by
typeand a longerexp— so without this a leaked refresh token would passas a bearer credential, defeating the short access-token TTL. The bundled test
issuer now stamps a
typeclaim (defaultaccess,?type=refreshto mint arejection-test token) so the hands-on flow still passes.
sub/scopecarry a CR/LF or other controlcharacter are rejected before forwarding (upstream header-splitting /
scope-smuggling).
issuer/gateway_origin/jwks_urimust be absolute http(s) URLs, and
resource_pathmust start with/and notend with
/(a trailing slash silently broke the PRM exact-match); negativeleeway_secondsrejected. Misconfig now surfaces at first request instead ofas an opaque per-request 503 (
jwks_uri) or a silent universal 401 (issuer).KONG_ADMIN_LISTENwas0.0.0.0:8001in the
local/authgatecompose variants, exposing the unauthenticatedadmin API (
POST /config) to sibling containers on the compose network;re-bound to container loopback to match the hardened base compose.
cold caller for that URI, not the whole gateway; PRM accepts HEAD alongside GET.
-trimpath -ldflags="-s -w"and pinned tothe final image's arch via
TARGETOS/TARGETARCH.always-on); hands-on working directories, container-network derivation
(Compose v1 + v2), Go version requirement (1.25.10), and the
plugin-info/arch troubleshooting commands fixed.
Surfaced but deliberately not changed (recorded for follow-up): single-flight
/ negative-cache the cold JWKS fetch so a black-holed issuer can't serialize cold
callers into linear latency; evict stale
jwksCacheentries + cancel the refreshgoroutine when
jwks_urichanges on a live pluginserver; bumpjwksetto aversion with atomic refresh (one malformed key mid-rotation can drop cached keys
for up to an hour); the
GetHeaders1000-header cap as a defense-in-depth limit(nginx already rejects duplicate
Authorizationfirst); collapsing the threenear-identical compose files into one parameterized file.
AI Authorship
Claude Code (Fable 5 / Opus 4.8) — multi-agent review with source-level
verification
kong-mcp/*, the rootREADME.md/.gitignoreedits, and thetype-claim addition ingo-jwks-multi/testissuerChange classification
else in the repo depends on it. Failure is local to this example.
Plan reference
Goal: give Kong OSS users an AuthGate-backed OAuth gateway for MCP servers, as an
offline-validation counterpart to Kong Enterprise's
ai-mcp-oauth2. Scope:new
kong-mcp/example only (plugin, declarative config, Docker demo, EN/zh-TWdocs) plus a row + section in the root README. The only change outside
kong-mcp/is an optional, default-accesstypeclaim added to the sharedgo-jwks-multi/testissuerso the hands-on demo can mint AuthGate-shaped tokens.Verification
go-*example dirs which ship without tests)668eede):cd kong-mcp && go mod tidy && go build -o mcp-authgate .→ builds cleango vet ./.../gofmt -l .→ no issues (both kong-mcp andgo-jwks-multi)
mcp-authgate -dump→ schema emits all 8 config fields, unexported state skippedHANDS-ON.zh-TW.mdvalidation matrix against the bundled test issuer;rows requiring a real AuthGate (§8.6) must be exercised before production
use — in particular confirm AuthGate emits
type=accessfor every flowthe gateway must accept.
Security check
example.complaceholders inkong.ymltype=access, plus scope (whenrequired_scopesset) and aud (whenrequire_audienceon)HS*refusedtype=accessis accepted, so a leakedrefresh token (same key/iss/aud/scope) can't be used as a bearer credential
sub/scopewith CR/LF or other controlchars are rejected before being forwarded as trusted headers
X-MCP-Subject/X-MCP-Scopecleared before the gateway sets its own values; a failed clear/set
answers 500 instead of proxying (fail closed)
Authorizationrejected (400) — a second, unvalidatedcredential can never be forwarded past the gateway
issuer/gateway_origin/jwks_urimust be absolute http(s) URLs andresource_pathis slash-checked; emptyissuercan no longer silentlyskip iss validation
infra failures answer 503 without JWKS URLs / dial errors
(base, local, authgate)
Risk & rollback
an optional
typeclaim on the shared test issuer (defaultaccess,backwards-compatible). No existing code path is touched.
kong-mcp/.Reviewer guide
kong-mcp/main.go— token validation, alg pinning,the
type=accessgate, control-char rejection, thesetup()configvalidation, JWKS client assembly (fail-fast options, per-URI lock),
401/403/503 error taxonomy, PRM serving, and the trusted-header clearing
(security-sensitive, AI-authored, not yet human-reviewed).
kong.yml/kong.local.yml/kong.authgate.yml,Dockerfile,.dockerignore, the threedocker-compose*.yml, READMEs,HANDS-ON.zh-TW.md.🤖 Generated with Claude Code