- Python 76.8%
- HTML 22.5%
- Dockerfile 0.7%
| e2e-app | ||
| mcp-server | ||
| monitoring | ||
| .gitignore | ||
| compose.monitoring.yml | ||
| compose.pocketid.yml | ||
| compose.yml | ||
| NEXT_STEPS.md | ||
| README.md | ||
Kong AI Gateway MCP test server
A small remote MCP server using the official Python MCP SDK and Streamable HTTP (not the legacy HTTP/SSE transport). It provides three tools: echo(message), add(a, b), and server_info().
The server uses stateless HTTP and JSON responses to keep proxy testing simple. It does not exercise persistent sessions or SSE streaming.
See NEXT_STEPS.md for the prioritized improvement roadmap and acceptance criteria.
Start the full feature set
From the project directory, launch Kong, MCP, the Pocket ID-enabled GUI, Grafana, Prometheus, Loki, and the log collector:
podman compose \
-f compose.yml \
-f compose.pocketid.yml \
-f compose.monitoring.yml \
--profile gui \
--profile monitoring \
up -d --build
| Service | URL |
|---|---|
| Playground GUI with Pocket ID | http://localhost:8080 |
| Kong chat/MCP gateway | http://localhost:8000 |
| Direct MCP debugging endpoint (unauthenticated, loopback-only) | http://localhost:8001/mcp |
| Grafana | http://localhost:3000 |
| Prometheus | http://localhost:9090 |
| Loki and log collector | Internal container network only |
Prerequisites: install Podman and a Compose provider; supply the Konnect cluster certificates under cert/; create the pocketid-client-secret Podman secret; and configure the Konnect authentication/forwarding policies. On the existing development machine these are already configured. On a fresh clone, follow the Pocket ID sections below and monitoring/README.md, including python3 monitoring/setup.py and python3 monitoring/attribution.py to prepare the monitoring secrets and policies. The Compose command does not recreate external secrets or Konnect configuration.
The test profile is intentionally omitted: it runs a one-shot CLI test, not a persistent service. Keep all three Compose files in full-stack commands, particularly when recreating Kong, so its metrics listener and GUI authentication configuration are retained.
Start the basic services with Podman
Install Podman and a Compose provider (podman-compose or another provider supported by podman compose), then run from this directory:
podman compose up -d --build
podman compose logs mcp-server
To start only the MCP server without starting Kong:
podman compose up -d --build mcp-server
Endpoints:
- Direct from your host:
http://localhost:8001/mcp - Upstream from Kong on the Compose network:
http://mcp-server:8000/mcp - Through Kong after creating the route below:
http://localhost:8000/mcp
The direct port is bound to loopback only. This test server has no authentication; do not expose it publicly without access controls. The container runs as a non-root user. The existing Kong certificate mount retains its SELinux labeling.
Configure Kong in Konnect
This project runs a Konnect-managed data plane, so create the service and route in the control plane associated with this data plane (not via a local Admin API).
- Create a Gateway Service named
test-mcpwith protocolhttp, hostmcp-server, and port8000. Leave the service path empty. - Create a Route on that service with path
/mcpand strip path disabled. Leave methods unrestricted so MCP POST, GET, and DELETE requests can reach the upstream. - Wait for the configuration to reach the data plane.
Preserving /mcp is important: stripping it would send requests to /, where there is no MCP endpoint. For a basic proxy test, no MCP-specific plugin is needed. Add your desired AI Gateway plugins separately once the baseline works.
If you configure authentication plugins on the Kong route, supply their required credentials when testing; the included smoke test assumes an unauthenticated route.
Test the full MCP protocol
Create a local Python environment (Python 3.10 or later):
python3 -m venv .venv
.venv/bin/pip install -r mcp-server/requirements.txt
Test directly, then through Kong:
.venv/bin/python mcp-server/smoke_test.py http://localhost:8001/mcp
.venv/bin/python mcp-server/smoke_test.py http://localhost:8000/mcp
Each test initializes an MCP session, lists the tools, and calls all three tools, checking their results. A nonzero exit status indicates a failure.
You can also connect an MCP client that supports Streamable HTTP to either URL above.
Quick curl check
curl -i http://localhost:8001/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
--data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl-test","version":"1.0.0"}}}'
Replace port 8001 with 8000 to check through Kong. Use POST with MCP headers: opening /mcp in a browser is not a meaningful tool-call test.
LLM + MCP end-to-end application
e2e-app/app.py is a CLI application that calls both Kong endpoints: chat completions and MCP. It initializes MCP, discovers tools and their schemas, supplies them to the LLM, executes requested tool calls, and returns the results until the LLM produces a final answer. The optional browser interface reuses this bridge; neither mode changes your Konnect configuration.
Your configured model alias is nanogpt, chat route is /v1/chat/completions, and MCP passthrough route is /mcp. Ensure Kong and the MCP server are running first. Requests go to your configured model provider and may incur inference costs; prompts and tool results are sent to that provider. Only use this application with trusted MCP servers: discovered tools can be executed at the model's request.
Browser GUI
With Kong and the MCP server already running:
podman compose --profile gui up -d --build e2e-gui
Open http://localhost:8080. Enter a prompt and click Run, or click E2E test for the verified add/echo scenario. The UI displays discovered tool schemas, live tool calls and results, the final answer, and pass/fail status. Each run is independent, not a persistent chat conversation. Existing CLI commands still work.
The GUI binds to loopback only and is intended for local testing, not public deployment. It permits one run at a time, bounds execution to five minutes, and cancels work when the browser disconnects (already executed tools cannot be undone). Endpoint configuration and optional gateway credentials stay server-side. To use authenticated endpoints, recreate the GUI with credentials forwarded from your host environment:
podman compose --profile gui run --rm --service-ports \
-e CHAT_API_KEY -e MCP_API_KEY e2e-gui
Stop the detached GUI first before using the foreground command, to free port 8080:
podman compose --profile gui stop e2e-gui
For local Python execution, install the application requirements then run .venv/bin/python e2e-app/web.py.
Pocket ID login: stage 1 (GUI only)
compose.pocketid.yml opts the GUI into Pocket ID login. The overlay controls GUI login only; native Kong OIDC strategy bindings independently protect the chat and MCP resources. Those bindings are now active in this environment. The normal Compose setup remains a login-disabled local demo; in OIDC mode, login and CSRF validation are required and shared credential fallback is not used.
The registered client is 0b1a7204-ee42-49fe-8c54-798533df499e, issuer https://id.caleb-brown.dev, callback http://localhost:8080/auth/callback. Ensure your user is allowed in Pocket ID's client Access tab.
Create a Podman secret using a hidden prompt (no secret in shell history or source files):
python3 -c 'import getpass, sys; sys.stdout.write(getpass.getpass("Pocket ID client secret: "))' \
| podman secret create pocketid-client-secret -
After creating it, enable the overlay and recreate only the GUI:
podman compose -f compose.yml -f compose.pocketid.yml --profile gui \
up -d --build --force-recreate e2e-gui
Open http://localhost:8080, click Sign in with Pocket ID, complete login, and verify the signed-in identity. The overlay now enables prompt execution because both native Kong bindings are active and reject missing/invalid tokens. Click Run E2E test to verify positive user authentication, model calls, tool calls, and upstream credential hiding. OIDC_GATEWAY_AUTH_READY is a deployment switch, not a runtime health assertion; keep it false in environments where the strategy has not been bound. Use localhost, not 127.0.0.1, to match the registered callback. The overlay mounts the external secret read-only; no real secret is included in project files or the image.
Authlib performs discovery, state and nonce checks, S256 PKCE, and ID-token validation. Provider tokens are kept in bounded in-memory server sessions; the HTTP-only, SameSite=Lax browser cookie contains an opaque session ID (and temporary login state), not access/refresh tokens. User access tokens are passed explicitly to both Kong endpoints. Sessions end on access-token expiry, after at most eight hours, or when the GUI restarts; sign in again when needed. Automatic refresh is intentionally not implemented in this first stage. Sign out ends the app session but not the Pocket ID SSO session. An in-flight run can complete after sign-out; logout does not undo requests already sent.
The local overlay enables ALLOW_UNAUTHENTICATED_TESTING. The Unauthenticated test mode checkbox unlocks discovery/run buttons without login and omits gateway credentials even when signed in. Discovery calls only MCP; either run button performs small negative probes against both MCP and chat, expecting HTTP 401/403. The usual bridge and tools are not executed in this mode. An accidentally unprotected chat route can incur a small inference charge. The testing requests still require same-origin/CSRF checks; normal requests still require login. Set the environment flag to "false" or remove it to disable this development-only feature. It does not change Kong authentication settings.
HTTP cookies are used only for the loopback development callback. A deployed app needs HTTPS, shared session storage for multiple replicas, and operational hardening. Do not expose this local development setup publicly.
To run tests without supplying a real secret or contacting Pocket ID:
podman compose --profile test run --rm --build --entrypoint python e2e-app \
-m unittest discover -v
To return to the login-disabled local demo (not appropriate once gateway auth is required):
podman compose --profile gui up -d --force-recreate e2e-gui
Pocket ID gateway authentication: stage 2
A native openid-connect strategy named pocketid-playground is bound to both chat and mcp-server. Live checks verified HTTP 401 on both endpoints with missing and invalid tokens. Positive authentication and credential hiding must be verified with a real signed-in E2E run. e2e-app/kong_auth.py is a host-side helper using your authenticated kongctl; it reads the existing mounted client secret into memory and sends the strategy payload over stdin. By default it only prepares an unbound strategy, without printing credentials or writing secret files. Explicit --bind uses typed PUT requests to preserve existing resource settings while changing auth bindings, verifies read-back, and attempts rollback on failure. It refuses to replace a different existing authentication binding. Konnect accepts binding IDs but returns canonical names.
python3 e2e-app/kong_auth.py
python3 e2e-app/kong_auth.py --bind
For an explicit local-test rollback, python3 e2e-app/kong_auth.py --unbind removes only this strategy's bindings. This restores unauthenticated access if no other auth strategy remains. It does not delete the strategy. Do not use this as an authentication-error workaround in a deployed environment.
The prepared strategy selects introspection with client-secret Basic authentication, HTTPS certificate verification, credential hiding, and an explicit audience requirement for the playground client. Introspection caching is disabled for this initial test. Konnect accepting the configuration does not prove successful runtime token validation.
The MCP passthrough listener's config.server.forward_client_headers is disabled. Neither this setting nor OIDC hide_credentials alone prevented an Authorization header reaching the MCP upstream in the observed runtime. Reapply this targeted setting with:
python3 e2e-app/kong_auth.py --disable-mcp-header-forwarding
This setting also affects forwarding of other client-supplied headers, so clients requiring custom upstream headers or stateful MCP session headers need additional validation/configuration. Our server is stateless.
The effective fix is a non-global Request Transformer policy named mcp-remove-caller-authorization, attached only to mcp-server, with config.remove.headers: ["Authorization"]. It removes the outbound header while leaving Pocket ID authentication enabled. Reapply the verified policy binding with:
python3 e2e-app/kong_auth.py --strip-mcp-authorization
A live authenticated MCP initialization and server_info call using a Pocket ID client-credentials token returned authorization_present: false; unauthenticated MCP requests still returned HTTP 401. No token or client secret was printed. Rerun the signed-in GUI E2E test to verify the browser user-token path and full LLM workflow. The policy is deliberately not global: the chat provider must retain its separately configured provider credential.
After signing in, click Check access token. The GUI backend contacts Pocket ID's introspection endpoint with your access token and the mounted client credentials. It returns only active, aud, client_id, and scope; neither tokens nor secrets are exposed. No model request or MCP tool execution occurs. This establishes whether your real access token matches the intended audience before enabling gateway authentication. If introspection is forbidden or aud is missing/different, investigate that rather than disabling audience checks.
One-command live test with Podman
podman compose --profile test run --rm --build e2e-app
The test requires actual successful add(27, 15) and echo("Hello from my LLM") calls, checks that the final answer includes both results, and requires server_info() to report authorization_present: false. That field exposes only a boolean, never credentials. With Pocket ID bindings active, use the signed-in GUI or provide a valid Pocket ID access token to the CLI; the unauthenticated CLI command above will now fail. It exits nonzero on failure. Tool calls are automatic, with a maximum of eight chat rounds and a five-minute total timeout. Logs show prompts' tool arguments and results; do not use sensitive test data.
Ask your own question
podman compose --profile test run --rm e2e-app \
--prompt 'Use server_info to identify the MCP server, then add 100 and 23.'
CLI authentication
The CLI does not perform interactive OIDC login. With the Pocket ID strategy active, set CHAT_API_KEY and MCP_API_KEY to an appropriate valid Pocket ID access token in your host environment, then forward them without placing tokens in the Compose file:
podman compose --profile test run --rm \
-e CHAT_API_KEY -e MCP_API_KEY e2e-app --test
Use gateway client credentials, not the upstream provider key configured in Konnect.
Grafana + Prometheus + Loki monitoring
An opt-in metrics stack is running at http://localhost:3000 (Grafana) and http://localhost:9090 (Prometheus). The provisioned dashboard is Kong AI Gateway — Local Overview. See monitoring/README.md for the admin password retrieval command, startup/stop instructions, persistence, and security notes.
The stack collects Kong gateway/MCP metrics and redacted, searchable request logs with application and pseudonymous subject attribution. Open http://localhost:3000/d/kong-request-attribution for the attribution dashboard. Distributed traces are not configured. When recreating Kong, include compose.monitoring.yml to retain its internal metrics listener.
Run locally or run unit tests
.venv/bin/pip install -r e2e-app/requirements.txt
.venv/bin/python e2e-app/app.py --test
.venv/bin/python -m unittest discover -s e2e-app -v
Local defaults use http://localhost:8000 for both endpoints. Override --chat-url, --mcp-url, or --model as needed. With Podman, run unit tests without making LLM requests:
podman compose --profile test run --rm --entrypoint python e2e-app \
-m unittest discover -v
Stop
podman compose down