feat(ci): add MCP protocol conformance tests#81
Conversation
Validate MCP protocol behavior (initialize, tools/list, tools/call, etc.) against the running server over streamable HTTP on every PR, with no Kubernetes cluster and no LLM. - Add .github/workflows/conformance.yaml: starts kubeflow-mcp over HTTP with no kubeconfig, waits for initialize, and runs modelcontextprotocol/conformance@v0.1.16 (active suite, Node 22). - Add tests/conformance/expected-failures.yaml baseline seeded from real runner output: fixture-driven scenarios, unadvertised capabilities, and the FastMCP DNS-rebinding hardening gap. CI fails on regressions and on baselined scenarios that unexpectedly start passing. - Add `make conformance` to run the suite locally. - Document conformance testing in CONTRIBUTING.md. Signed-off-by: Kartikeya Trivedi <kartikeyatrivedi4oct2004@gmail.com>
|
🎉 Welcome to the Kubeflow MCP Server! 🎉 Thanks for opening your first PR! We're happy to have you as part of our community 🚀 Here's what happens next:
Join the community:
Feel free to ask questions in the comments if you need any help or clarification! |
There was a problem hiding this comment.
Pull request overview
Adds MCP protocol conformance testing while fixing HTTP middleware registration and DNS rebinding protection.
Changes:
- Adds CI and local conformance test execution with expected-failure baselines.
- Adds configurable Host/Origin validation for HTTP and SSE.
- Corrects
AuditIdentityMiddlewareregistration.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
.github/workflows/conformance.yaml |
Runs conformance tests in CI. |
tests/conformance/expected-failures.yaml |
Defines known conformance gaps. |
Makefile |
Adds local conformance target. |
CONTRIBUTING.md |
Documents conformance and security configuration. |
kubeflow_mcp/core/transport_security.py |
Implements transport security middleware. |
kubeflow_mcp/core/config.py |
Adds transport security configuration. |
kubeflow_mcp/cli.py |
Wires security into HTTP/SSE transports. |
kubeflow_mcp/core/server.py |
Registers audit middleware correctly. |
kubeflow_mcp/core/middleware.py |
Corrects middleware usage example. |
tests/unit/core/test_transport_security.py |
Tests transport security behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
FastMCP's streamable-HTTP transport ships DNS rebinding protection off by default, so a locally-bound, unauthenticated server accepted requests with an arbitrary Host/Origin (HTTP 200) and was reachable from a malicious web page via DNS rebinding. The MCP conformance `dns-rebinding-protection` scenario failed and had been baselined as a known gap. Add a pure-ASGI middleware that validates Host/Origin before requests reach the MCP handler, delegating the header checks to the MCP SDK's reference TransportSecurityMiddleware so behaviour matches the spec (421 for a bad Host, 403 for a bad Origin, 400 for a non-JSON POST). It is wired into the http/sse transports only; stdio is unaffected. Protection is on by default and allows loopback hosts; the allowlists and the on/off switch are configurable via KUBEFLOW_MCP_ALLOWED_HOSTS / KUBEFLOW_MCP_ALLOWED_ORIGINS / KUBEFLOW_MCP_DNS_REBINDING_PROTECTION. Verified end-to-end: the live server now returns 421/403 for bad Host/Origin and the conformance scenario passes (2/2), so it is removed from the baseline and is now a required pass that CI will guard against regression. Signed-off-by: Kartikeya Trivedi <kartikeyatrivedi4oct2004@gmail.com>
create_server() passed the AuditIdentityMiddleware class to
mcp.add_middleware() rather than an instance. FastMCP invokes middleware as
middleware(context, call_next), so every tools/list, tools/call, resources/*,
and prompts/* request over HTTP attempted to instantiate the class with two
arguments and failed with a JSON-RPC error:
{"error": {"code": 0, "message": "AuditIdentityMiddleware() takes no arguments"}}
This was caught by the MCP conformance suite added in this PR: on current
main, every scenario beyond initialize/ping fails. With this one-line fix
(plus the docstring example), the server answers protocol requests again and
the conformance suite returns to its expected baseline.
Signed-off-by: Kartikeya Trivedi <kartikeyatrivedi4oct2004@gmail.com>
c57d4eb to
0b64e30
Compare
|
/ok-to-test |
…ogger Review follow-ups from kubeflow#81: - Document KUBEFLOW_MCP_ALLOWED_HOSTS / KUBEFLOW_MCP_ALLOWED_ORIGINS / KUBEFLOW_MCP_DNS_REBINDING_PROTECTION in the README environment-variable table, and add an in-cluster note: the defaults are loopback-only, so a Service/Ingress Host must be allowlisted or requests are rejected with 421. - Remove the unused module logger from transport_security.py. - Add load_config() tests covering the new security env vars: CSV parsing (with whitespace and trailing commas), the [::1] bracketed origin form, and the enable/disable boolean spellings. Signed-off-by: Kartikeya Trivedi <kartikeyatrivedi4oct2004@gmail.com>
|
Thanks @abhijeet-dhumal! Documented all three env vars (KUBEFLOW_MCP_ALLOWED_HOSTS / KUBEFLOW_MCP_ALLOWED_ORIGINS / KUBEFLOW_MCP_DNS_REBINDING_PROTECTION) in the README env table in 73aa14e. Also added a note under the in-cluster deployment section with an example allowlist, since the loopback-only defaults mean a Service/Ingress Host would 421 without one, agreed that's where operators would actually get bitten. The inline nits are addressed in the same commit |
|
Conformance suite + DNS rebinding protection are solid additions and this also fixes #79 /lgtm |
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: abhijeet-dhumal The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
What this PR does
Adds MCP protocol conformance tests to CI using the official MCP conformance framework, validating that the server correctly implements the protocol (
initialize,tools/list,tools/call, etc.) on every PR — with no Kubernetes cluster and no LLM.Running the suite immediately caught two real bugs, both fixed here:
mainis currently broken over HTTP: everytools/list,tools/call,resources/*, andprompts/*request fails withAuditIdentityMiddleware() takes no arguments—create_server()registers the middleware class instead of an instance, so FastMCP'smiddleware(context, call_next)call tries to instantiate it with two arguments (regression from feat: add OpenTelemetry tracing for tool calls #21).Host/Originheader by default, leaving a locally-bound, unauthenticated server reachable from a malicious web page via DNS rebinding.Fixes #62
Changes
feat(ci): add MCP protocol conformance tests.github/workflows/conformance.yaml— startskubeflow-mcp serve --transport httpwith no kubeconfig, waits for it to answerinitialize, then runsmodelcontextprotocol/conformance@v0.1.16(activesuite) againsthttp://localhost:8000/mcp. Server logs are dumped on failure.tests/conformance/expected-failures.yaml— baseline seeded from real runner output. Every entry is a legitimate gap for this server (scenarios assuming the reference server's fixtures, plus unadvertised capabilities), each with a one-line reason. CI fails on regressions and on baselined scenarios that unexpectedly start passing.make conformance— run the same suite locally (Node.js 20+).fix(security): enforce Host/Origin validation on HTTP transportkubeflow_mcp/core/transport_security.py— pure-ASGI middleware (no response buffering, SSE-safe) that validatesHost/Originbefore requests reach the MCP handler, delegating header checks to the MCP SDK's referenceTransportSecurityMiddleware: 421 invalid Host, 403 invalid Origin, 400 non-JSON POST.http/ssetransports only;stdiountouched. On by default with loopback allowlists (correct for the default127.0.0.1bind); configurable viaKUBEFLOW_MCP_ALLOWED_HOSTS/KUBEFLOW_MCP_ALLOWED_ORIGINS(comma-separated,:*port wildcard) andKUBEFLOW_MCP_DNS_REBINDING_PROTECTION.dns-rebinding-protectionis intentionally not baselined — it is a required pass, so CI guards against regression.fix(core): register AuditIdentityMiddleware instance instead of classcreate_server()(mcp.add_middleware(AuditIdentityMiddleware())) plus the docstring example. Without it the conformance job can never pass onmain— reproducible today with a plaincurltools/listagainstkubeflow-mcp serve --transport http.Verification
Tests written in this PR, run locally against the exact committed code (rebased on current
main):Unit tests —
tests/unit/core/test_transport_security.py:Full suite: 186 passed.
make verifyclean (uv lock, ruff check, ruff format).Conformance suite (
active, v0.1.16) — exit 0,Baseline check passed: all failures are expected.Passing scenarios:All remaining failures are baselined with reasons (reference-server fixtures / unadvertised capabilities).
Manual probes against the live server:
References