Skip to content

Commit 7fbb3ce

Browse files
authored
feat: add MCP HTTP server experiment and improve experiment middleware (#18712)
# Add MCP HTTP Server Experiment This PR adds a new experiment flag `mcp-server-http` to enable the MCP HTTP server functionality. The changes include: 1. Added a new experiment constant `ExperimentMCPServerHTTP` with the value "mcp-server-http" 2. Added display name and documentation for the new experiment 3. Improved the experiment middleware to: - Support requiring multiple experiments - Provide better error messages with experiment display names - Add a development mode bypass option 4. Applied the new experiment requirement to the MCP HTTP endpoint 5. Replaced the custom OAuth2 middleware with the standard experiment middleware The PR also improves the `Enabled()` method on the `Experiments` type by using `slices.Contains()` for better readability.
1 parent 1555154 commit 7fbb3ce

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)