Skip to content

Commit 610c661

Browse files
committed
feat(mcp): add experiment control for MCP server HTTP endpoints
- Add ExperimentMCPServerHTTP constant for controlled rollout - Refactor OAuth2 middleware into generic experiment middleware - Make experiment middleware variadic to support multiple experiments - Apply experiment gating to /api/experimental/mcp/http routes - Maintain development mode bypass for testing flexibility - Remove OAuth2-specific middleware in favor of reusable pattern Change-Id: Ia5b3d0615f4a5a45e5a233b1ea92e8bdc0a5f17e Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parent 51f84ce commit 610c661

File tree

8 files changed

+87
-33
lines changed

8 files changed

+87
-33
lines changed

coderd/apidoc/docs.go

Lines changed: 5 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 5 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/coderd.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -922,7 +922,7 @@ func New(options *Options) *API {
922922
// logging into Coder with an external OAuth2 provider.
923923
r.Route("/oauth2", func(r chi.Router) {
924924
r.Use(
925-
api.oAuth2ProviderMiddleware,
925+
httpmw.RequireExperimentWithDevBypass(api.Experiments, codersdk.ExperimentOAuth2),
926926
)
927927
r.Route("/authorize", func(r chi.Router) {
928928
r.Use(
@@ -973,6 +973,9 @@ func New(options *Options) *API {
973973
r.Get("/prompts", api.aiTasksPrompts)
974974
})
975975
r.Route("/mcp", func(r chi.Router) {
976+
r.Use(
977+
httpmw.RequireExperimentWithDevBypass(api.Experiments, codersdk.ExperimentOAuth2, codersdk.ExperimentMCPServerHTTP),
978+
)
976979
// MCP HTTP transport endpoint with mandatory authentication
977980
r.Mount("/http", api.mcpHTTPHandler())
978981
})
@@ -1473,7 +1476,7 @@ func New(options *Options) *API {
14731476
r.Route("/oauth2-provider", func(r chi.Router) {
14741477
r.Use(
14751478
apiKeyMiddleware,
1476-
api.oAuth2ProviderMiddleware,
1479+
httpmw.RequireExperimentWithDevBypass(api.Experiments, codersdk.ExperimentOAuth2),
14771480
)
14781481
r.Route("/apps", func(r chi.Router) {
14791482
r.Get("/", api.oAuth2ProviderApps)

coderd/httpmw/experiments.go

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,59 @@ package httpmw
33
import (
44
"fmt"
55
"net/http"
6+
"strings"
67

8+
"github.com/coder/coder/v2/buildinfo"
79
"github.com/coder/coder/v2/coderd/httpapi"
810
"github.com/coder/coder/v2/codersdk"
911
)
1012

11-
func RequireExperiment(experiments codersdk.Experiments, experiment codersdk.Experiment) func(next http.Handler) http.Handler {
13+
// RequireExperiment returns middleware that checks if all required experiments are enabled.
14+
// If any experiment is disabled, it returns a 403 Forbidden response with details about the missing experiments.
15+
func RequireExperiment(experiments codersdk.Experiments, requiredExperiments ...codersdk.Experiment) func(next http.Handler) http.Handler {
1216
return func(next http.Handler) http.Handler {
1317
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
14-
if !experiments.Enabled(experiment) {
15-
httpapi.Write(r.Context(), w, http.StatusForbidden, codersdk.Response{
16-
Message: fmt.Sprintf("Experiment '%s' is required but not enabled", experiment),
17-
})
18-
return
18+
for _, experiment := range requiredExperiments {
19+
if !experiments.Enabled(experiment) {
20+
var experimentNames []string
21+
for _, exp := range requiredExperiments {
22+
experimentNames = append(experimentNames, string(exp))
23+
}
24+
25+
// Print a message that includes the experiment names
26+
// even if some experiments are already enabled.
27+
var message string
28+
if len(requiredExperiments) == 1 {
29+
message = fmt.Sprintf("%s functionality requires enabling the '%s' experiment.",
30+
requiredExperiments[0].DisplayName(), requiredExperiments[0])
31+
} else {
32+
message = fmt.Sprintf("This functionality requires enabling the following experiments: %s",
33+
strings.Join(experimentNames, ", "))
34+
}
35+
36+
httpapi.Write(r.Context(), w, http.StatusForbidden, codersdk.Response{
37+
Message: message,
38+
})
39+
return
40+
}
1941
}
42+
2043
next.ServeHTTP(w, r)
2144
})
2245
}
2346
}
47+
48+
// RequireExperimentWithDevBypass checks if ALL the given experiments are enabled,
49+
// but bypasses the check in development mode (buildinfo.IsDev()).
50+
func RequireExperimentWithDevBypass(experiments codersdk.Experiments, requiredExperiments ...codersdk.Experiment) func(next http.Handler) http.Handler {
51+
return func(next http.Handler) http.Handler {
52+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
53+
if buildinfo.IsDev() {
54+
next.ServeHTTP(w, r)
55+
return
56+
}
57+
58+
RequireExperiment(experiments, requiredExperiments...)(next).ServeHTTP(w, r)
59+
})
60+
}
61+
}

coderd/oauth2.go

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import (
1616

1717
"github.com/sqlc-dev/pqtype"
1818

19-
"github.com/coder/coder/v2/buildinfo"
2019
"github.com/coder/coder/v2/coderd/audit"
2120
"github.com/coder/coder/v2/coderd/database"
2221
"github.com/coder/coder/v2/coderd/database/db2sdk"
@@ -37,19 +36,6 @@ const (
3736
displaySecretLength = 6 // Length of visible part in UI (last 6 characters)
3837
)
3938

40-
func (api *API) oAuth2ProviderMiddleware(next http.Handler) http.Handler {
41-
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
42-
if !api.Experiments.Enabled(codersdk.ExperimentOAuth2) && !buildinfo.IsDev() {
43-
httpapi.Write(r.Context(), rw, http.StatusForbidden, codersdk.Response{
44-
Message: "OAuth2 provider functionality requires enabling the 'oauth2' experiment.",
45-
})
46-
return
47-
}
48-
49-
next.ServeHTTP(rw, r)
50-
})
51-
}
52-
5339
// @Summary Get OAuth2 applications.
5440
// @ID get-oauth2-applications
5541
// @Security CoderSessionToken

codersdk/deployment.go

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3342,8 +3342,30 @@ const (
33423342
ExperimentWorkspaceUsage Experiment = "workspace-usage" // Enables the new workspace usage tracking.
33433343
ExperimentWebPush Experiment = "web-push" // Enables web push notifications through the browser.
33443344
ExperimentOAuth2 Experiment = "oauth2" // Enables OAuth2 provider functionality.
3345+
ExperimentMCPServerHTTP Experiment = "mcp-server-http" // Enables the MCP HTTP server functionality.
33453346
)
33463347

3348+
func (e Experiment) DisplayName() string {
3349+
switch e {
3350+
case ExperimentExample:
3351+
return "Example Experiment"
3352+
case ExperimentAutoFillParameters:
3353+
return "Auto-fill Template Parameters"
3354+
case ExperimentNotifications:
3355+
return "SMTP and Webhook Notifications"
3356+
case ExperimentWorkspaceUsage:
3357+
return "Workspace Usage Tracking"
3358+
case ExperimentWebPush:
3359+
return "Browser Push Notifications"
3360+
case ExperimentOAuth2:
3361+
return "OAuth2 Provider Functionality"
3362+
case ExperimentMCPServerHTTP:
3363+
return "MCP HTTP Server Functionality"
3364+
default:
3365+
return string(e)
3366+
}
3367+
}
3368+
33473369
// ExperimentsKnown should include all experiments defined above.
33483370
var ExperimentsKnown = Experiments{
33493371
ExperimentExample,
@@ -3352,6 +3374,7 @@ var ExperimentsKnown = Experiments{
33523374
ExperimentWorkspaceUsage,
33533375
ExperimentWebPush,
33543376
ExperimentOAuth2,
3377+
ExperimentMCPServerHTTP,
33553378
}
33563379

33573380
// ExperimentsSafe should include all experiments that are safe for
@@ -3369,14 +3392,9 @@ var ExperimentsSafe = Experiments{}
33693392
// @typescript-ignore Experiments
33703393
type Experiments []Experiment
33713394

3372-
// Returns a list of experiments that are enabled for the deployment.
3395+
// Enabled returns a list of experiments that are enabled for the deployment.
33733396
func (e Experiments) Enabled(ex Experiment) bool {
3374-
for _, v := range e {
3375-
if v == ex {
3376-
return true
3377-
}
3378-
}
3379-
return false
3397+
return slices.Contains(e, ex)
33803398
}
33813399

33823400
func (c *Client) Experiments(ctx context.Context) (Experiments, error) {

docs/reference/api/schemas.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/src/api/typesGenerated.ts

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy