Skip to content

refactor(httpapi): type the controller seams and fail closed without a config - #1346

Open
Dumbris wants to merge 1 commit into
mainfrom
fix/typed-controller-seams
Open

Dumbris wants to merge 1 commit into
mainfrom
fix/typed-controller-seams

Conversation

@Dumbris

@Dumbris Dumbris commented Sep 22, 2026

Copy link
Copy Markdown
Member

What

Types two interface{} seams on httpapi.ServerController and makes the auth middleware fail closed when it has no configuration.

ARC-04 — the management service travelled as interface{}. It left internal/runtime untyped, crossed ServerController, and was re-derived in internal/httpapi by 12 ad-hoc anonymous interface assertions. Five of those were unchecked (mgmtSvc.(interface{...}).Method(...)), so a method-set drift panicked into chi's Recoverer and surfaced as an opaque 500 with no log line naming the cause; the other seven degraded to ok=false and a 500. management.Service already describes exactly what those handlers call, and internal/httpapi already imports internal/management, so the seam is now typed to it end to end and all 12 assertions are gone. The three equivalents on the MCP surface (internal/server/mcp.go) go with them.

SEC-02 — two fail-open branches in apiKeyAuthMiddleware. Typing GetCurrentConfig() to *config.Config makes the "not the expected type" branch unrepresentable; the remaining nil path now returns 503 instead of forwarding the request. To be precise about severity: in production Runtime.GetCurrentConfig() wrapped a *config.Config and runtime.New rejects a nil config, so this was not an exploitable bypass there — but it was a live one for any other implementor of the interface, and 37 existing test requests were authenticating by not authenticating.

Why it is one change

Both defects are the same missing type. The named interface the seam needed already existed; nothing new was designed.

Notable

  • handleGetGlobalTools keeps its deliberate graceful fallback to controller.GetServerTools; the nil check stays on the interface value.
  • Three security tests (Spec 099 FR-018a's disclosure floor, and auth.AuthorizeServerOp's unrestricted-on-absent-AuthContext default) reached their handlers through the deleted branch. They are rewritten, not deleted: they now drive the handler and the subtree gate directly with a no-AuthContext request and keep the same assertions. index_search_scoped moved for a different reason — the middleware would replace the AuthContext those tests inject and turn every scoped fixture into an admin one.
  • No REST contract, OAS or frontend change. The only new status code is the 503.

How verified

TDD: both tests were written and run red first (503-and-handler-not-reached failed 200/true; the compile-time contract failed to build).

  • go test -race on internal/httpapi, internal/runtime, internal/management, and internal/server with the unit-tests.yml skip regex
  • the server-edition race matrix under -tags server (which is where index_search_scoped_test.go lives — invisible to a bare run)
  • both golangci-lint passes, bare and --build-tags server; the 16/19 findings are pre-existing and untouched
  • live instance: all 12 converted call sites exercised over REST and MCP against a real mcpproxy serveservers, doctor, {id}/tools, enable/disable, restart, restart_all, enable_all, disable_all, logout, global tools, and upstream_servers restart/enable/disable over JSON-RPC. Unauthenticated request still 401s; zero panics in the log.

Cross-model review: codex gpt-5.6-sol, one round, no production findings. It raised one P3 — the rewritten tests no longer traverse the full chi route. I verified it and did not change the code: at least five other tests (scope_reveal_test.go:341, contracts_test.go:429, search_tools_test.go:48, security_test.go:57, tenant_allowlist_walk_test.go:129) already drive /api/v1/index/search through the real router, and re-plumbing the rewritten tests would destroy the very property they pin.

🤖 Generated with Claude Code

…a config

The management service travelled from internal/runtime through
httpapi.ServerController as interface{}, and twelve handler sites in
internal/httpapi re-derived its method set with ad-hoc anonymous interface
assertions. Five of those were unchecked, so a method-set drift became a
panic recovered by chi into an opaque 500 with no log line naming the cause;
the other seven degraded to a runtime ok=false and a 500.

management.Service already describes exactly what those handlers call and
internal/httpapi already imports internal/management, so the seam is typed
to it end to end (runtime field + setter + getter, internal/server.Server,
httpapi.ServerController) and all twelve assertions are deleted. The three
equivalents on the MCP surface in internal/server/mcp.go go with them.

apiKeyAuthMiddleware had two "allow through (testing scenario)" branches on
GetCurrentConfig(). Typing the getter to *config.Config makes the
non-*config.Config branch unrepresentable, and the remaining nil path now
fails CLOSED with 503: no configuration means there is nothing to
authenticate against, so the request cannot be authenticated. In production
Runtime.GetCurrentConfig() wrapped a *config.Config and runtime.New rejects
a nil config, so this was not an exploitable bypass there — but it was a
live one for any other implementor, and 37 test requests authenticated by
not authenticating.

Three security tests (Spec 099 FR-018a's disclosure floor, and
auth.AuthorizeServerOp's unrestricted-on-absent-AuthContext default) reached
their handlers through the deleted branch. They are rewritten to drive the
handler and the subtree gate directly with a no-AuthContext request, keeping
the same assertions, rather than deleted with the branch. index_search_scoped
does the same: the middleware would otherwise replace the AuthContext those
tests inject and turn every scoped fixture into an admin one.

Verified on a live instance: all twelve converted call sites exercised over
REST and MCP, auth still required, no panics.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 971560c
Status: ✅  Deploy successful!
Preview URL: https://9d5ae356.mcpproxy-docs.pages.dev
Branch Preview URL: https://fix-typed-controller-seams.mcpproxy-docs.pages.dev

View logs

@github-actions

Copy link
Copy Markdown
Contributor

📦 Build Artifacts

Workflow Run: View Run
Branch: fix/typed-controller-seams

Available Artifacts

  • archive-darwin-amd64 (30 MB)
  • archive-darwin-arm64 (27 MB)
  • archive-linux-amd64 (18 MB)
  • archive-linux-arm64 (16 MB)
  • archive-windows-amd64 (30 MB)
  • archive-windows-arm64 (26 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (24 MB)
  • installer-dmg-darwin-arm64 (22 MB)
  • smart-mcp-proxymcpproxy-go5Y2YDB.dockerbuild (0 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 35748149989 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 62.22222% with 17 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/httpapi/server.go 69.69% 4 Missing and 6 partials ⚠️
internal/runtime/runtime.go 0.00% 3 Missing ⚠️
internal/server/mcp.go 0.00% 3 Missing ⚠️
internal/server/update_failure.go 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants