<#1104 Fix osctrl-api startup panic when hosted MC...
# osctrl
g
#1104 Fix osctrl-api startup panic when hosted MCP is enabled Pull request opened by javuto Fix osctrl-api startup panic when hosted MCP is enabled Fixes #1099. Problem With
MCP_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 pattern
The 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/osctrl