Skip to content

Commit 9965337

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 1555154 commit 9965337

File tree

9 files changed

+93
-34
lines changed

9 files changed

+93
-34
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: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import (
1616

1717
"github.com/google/uuid"
1818
"golang.org/x/mod/semver"
19+
"golang.org/x/text/cases"
20+
"golang.org/x/text/language"
1921
"golang.org/x/xerrors"
2022

2123
"github.com/coreos/go-oidc/v3/oidc"
@@ -3342,8 +3344,33 @@ const (
33423344
ExperimentWorkspaceUsage Experiment = "workspace-usage" // Enables the new workspace usage tracking.
33433345
ExperimentWebPush Experiment = "web-push" // Enables web push notifications through the browser.
33443346
ExperimentOAuth2 Experiment = "oauth2" // Enables OAuth2 provider functionality.
3347+
ExperimentMCPServerHTTP Experiment = "mcp-server-http" // Enables the MCP HTTP server functionality.
33453348
)
33463349

3350+
func (e Experiment) DisplayName() string {
3351+
switch e {
3352+
case ExperimentExample:
3353+
return "Example Experiment"
3354+
case ExperimentAutoFillParameters:
3355+
return "Auto-fill Template Parameters"
3356+
case ExperimentNotifications:
3357+
return "SMTP and Webhook Notifications"
3358+
case ExperimentWorkspaceUsage:
3359+
return "Workspace Usage Tracking"
3360+
case ExperimentWebPush:
3361+
return "Browser Push Notifications"
3362+
case ExperimentOAuth2:
3363+
return "OAuth2 Provider Functionality"
3364+
case ExperimentMCPServerHTTP:
3365+
return "MCP HTTP Server Functionality"
3366+
default:
3367+
// Split on hyphen and convert to title case
3368+
// e.g. "web-push" -> "Web Push", "mcp-server-http" -> "Mcp Server Http"
3369+
caser := cases.Title(language.English)
3370+
return caser.String(strings.ReplaceAll(string(e), "-", " "))
3371+
}
3372+
}
3373+
33473374
// ExperimentsKnown should include all experiments defined above.
33483375
var ExperimentsKnown = Experiments{
33493376
ExperimentExample,
@@ -3352,6 +3379,7 @@ var ExperimentsKnown = Experiments{
33523379
ExperimentWorkspaceUsage,
33533380
ExperimentWebPush,
33543381
ExperimentOAuth2,
3382+
ExperimentMCPServerHTTP,
33553383
}
33563384

33573385
// ExperimentsSafe should include all experiments that are safe for
@@ -3369,14 +3397,9 @@ var ExperimentsSafe = Experiments{}
33693397
// @typescript-ignore Experiments
33703398
type Experiments []Experiment
33713399

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

33823405
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.

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ require (
206206
golang.org/x/sync v0.14.0
207207
golang.org/x/sys v0.33.0
208208
golang.org/x/term v0.32.0
209-
golang.org/x/text v0.25.0 // indirect
209+
golang.org/x/text v0.25.0
210210
golang.org/x/tools v0.33.0
211211
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da
212212
google.golang.org/api v0.231.0

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