GitHub
10/23/2026, 1:55 PMMCP_ENABLED=true, osctrl-api panics while registering routes and exits with code 2 before its listener starts:
panic: pattern "/api/v1/mcp" conflicts with pattern "GET /": /api/v1/mcp matches more methods than GET /, but has a more specific path patternThe API registers the root handler as
GET / and, when MCP is enabled, the MCP endpoint with no method (/api/v1/mcp). Go 1.22's ServeMux treats those two patterns as ambiguous, because one matches more methods and the other has a more specific path, so Handle panics. The failure happens before any request reaches authentication or MCP, and it does not depend on MCP_ALLOW_WRITES.
What changes
• Method-qualified MCP routes. The endpoint is registered as POST /api/v1/mcp, GET /api/v1/mcp and DELETE /api/v1/mcp. These are the methods the MCP Go SDK's streamable handler serves (client messages, the server-to-client stream, and ending a session). All three share one handlerAuthCheck(mcpHandler(...)) instance, because the SDK keeps its session map on the handler, so a GET or DELETE has to reach the same instance as the POST that created the session.
• Unchanged gates. Routes are still registered only when MCP.Enabled is true. The write opt-in, authentication, per-endpoint permission checks (tool calls loop back through the same mux) and audit are untouched. A comment above the block explains why the patterns must stay method-qualified.
• Route registration moved into registerRoutes (cmd/api/main.go) so the complete production route set can be built in a test, following the cmd/tls precedent. It is a pure move: the registration region is byte-identical apart from the MCP block, with the same order and conditions. The values the body needs from the surrounding function are passed in as parameters: the mux, handlers, flag parameters, event bus, alerts manager, the log sinks and auth providers flags, and the audit log.
• MCP.md documents the methods and the 405 behavior.
Behavior
• Methods other than POST, GET and DELETE on /api/v1/mcp (for example PUT, PATCH, OPTIONS) get the mux's automatic 405.
• HEAD is routed to the GET pattern by Go, so it passes through the same auth check (401 without a token) and then gets the SDK's 405.
• With MCP disabled nothing changes: there are no MCP routes, a GET falls through to the root handler's 404, and a POST gets the mux's 405.
Security and operational impact
• No auth, permission or write-gating changes. The same handler chain wraps every MCP method, and unauthenticated POST, GET, DELETE and HEAD are rejected with 401 by handlerAuthCheck.
• An unauthenticated PUT or PATCH now gets 405 from the mux instead of 401. This only shows that the route exists, which a 401 on POST already shows.
• Deployments on v0.6.0 with MCP enabled currently cannot start the API. This fix restores startup.
Validation
• `go test ./... -count=1`: 58 packages pass, 0 failures. The new route tests also pass with -count=3 and with -race.
• go vet ./..., gofmt -l cmd pkg (empty), `golangci-lint run ./...`: 0 issues.
• `make openapi-check`: clean. The route move does not change the generated API documentation.
• New tests in cmd/api/routes_test.go, run against the real `registerRoutes`:
• `TestRegisterRoutesAllEnabled`: registration does not panic with MCP unset, disabled, read-only and with writes allowed, with every conditional route group enabled. A conflicting pattern anywhere in the route set now fails this test.
• `TestRegisterRoutesMCPEndpoint`: MCP disabled behaves as described above. With MCP enabled, POST, GET, DELETE and HEAD return 401 without a token, and PUT and PATCH return 405.
• `TestRegisterRoutesKeepsExistingRoutes`: a sample of existing routes (root, health, login, events, nodes, OIDC, SAML, audit logs, alerts, log sinks, auth providers) still matches its expected pattern, and the root, health, events and nodes routes still answer as before.
• `TestRegisterRoutesMCPRoundTrip`: a real MCP client connects (initialize) through the registered mux with a real JWT, a tool call loops back into the same mux and returns data, and a DELETE with the session id returns 204.
• The regression test was run against the unfixed registration first and failed with the exact panic from the issue. Three further mutations each made a test fail: going back to the methodless pattern, giving each method its own handler instance, and removing the auth wrapper.
jmpsec/osctrlGitHub
10/23/2026, 2:44 PM