diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 7e7a7ece0d..49ce14b2f5 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -81,6 +81,11 @@ OPTIONS: check is performed once per day. AIBRIDGE OPTIONS: + --aibridge-inject-coder-mcp-tools bool, $CODER_AIBRIDGE_INJECT_CODER_MCP_TOOLS (default: false) + Whether to inject Coder's MCP tools into intercepted AI Bridge + requests (requires the "oauth2" and "mcp-server-http" experiments to + be enabled). + --aibridge-anthropic-base-url string, $CODER_AIBRIDGE_ANTHROPIC_BASE_URL (default: https://api.anthropic.com/) The base URL of the Anthropic API. diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index 225c240d9e..33f5c56c43 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -747,3 +747,7 @@ aibridge: # https://docs.claude.com/en/docs/claude-code/settings#environment-variables. # (default: global.anthropic.claude-haiku-4-5-20251001-v1:0, type: string) bedrock_small_fast_model: global.anthropic.claude-haiku-4-5-20251001-v1:0 + # Whether to inject Coder's MCP tools into intercepted AI Bridge requests + # (requires the "oauth2" and "mcp-server-http" experiments to be enabled). + # (default: false, type: bool) + inject_coder_mcp_tools: false diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index ff4c39d7c9..9e61867557 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -11700,6 +11700,9 @@ const docTemplate = `{ "enabled": { "type": "boolean" }, + "inject_coder_mcp_tools": { + "type": "boolean" + }, "openai": { "$ref": "#/definitions/codersdk.AIBridgeOpenAIConfig" } diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index a4c639d3fe..3d6e076ccc 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -10396,6 +10396,9 @@ "enabled": { "type": "boolean" }, + "inject_coder_mcp_tools": { + "type": "boolean" + }, "openai": { "$ref": "#/definitions/codersdk.AIBridgeOpenAIConfig" } diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 9425a3740f..4d79058b68 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -3339,6 +3339,16 @@ Write out the current server config as YAML to stdout.`, Group: &deploymentGroupAIBridge, YAML: "bedrock_small_fast_model", }, + { + Name: "AI Bridge Inject Coder MCP tools", + Description: "Whether to inject Coder's MCP tools into intercepted AI Bridge requests (requires the \"oauth2\" and \"mcp-server-http\" experiments to be enabled).", + Flag: "aibridge-inject-coder-mcp-tools", + Env: "CODER_AIBRIDGE_INJECT_CODER_MCP_TOOLS", + Value: &c.AI.BridgeConfig.InjectCoderMCPTools, + Default: "false", + Group: &deploymentGroupAIBridge, + YAML: "inject_coder_mcp_tools", + }, { Name: "Enable Authorization Recordings", Description: "All api requests will have a header including all authorization calls made during the request. " + @@ -3358,10 +3368,11 @@ Write out the current server config as YAML to stdout.`, } type AIBridgeConfig struct { - Enabled serpent.Bool `json:"enabled" typescript:",notnull"` - OpenAI AIBridgeOpenAIConfig `json:"openai" typescript:",notnull"` - Anthropic AIBridgeAnthropicConfig `json:"anthropic" typescript:",notnull"` - Bedrock AIBridgeBedrockConfig `json:"bedrock" typescript:",notnull"` + Enabled serpent.Bool `json:"enabled" typescript:",notnull"` + OpenAI AIBridgeOpenAIConfig `json:"openai" typescript:",notnull"` + Anthropic AIBridgeAnthropicConfig `json:"anthropic" typescript:",notnull"` + Bedrock AIBridgeBedrockConfig `json:"bedrock" typescript:",notnull"` + InjectCoderMCPTools serpent.Bool `json:"inject_coder_mcp_tools" typescript:",notnull"` } type AIBridgeOpenAIConfig struct { diff --git a/docs/reference/api/general.md b/docs/reference/api/general.md index 5718979ae8..a0a24e6977 100644 --- a/docs/reference/api/general.md +++ b/docs/reference/api/general.md @@ -175,6 +175,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "small_fast_model": "string" }, "enabled": true, + "inject_coder_mcp_tools": true, "openai": { "base_url": "string", "key": "string" diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index e0b8a7e6c8..bdab37f297 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -389,6 +389,7 @@ "small_fast_model": "string" }, "enabled": true, + "inject_coder_mcp_tools": true, "openai": { "base_url": "string", "key": "string" @@ -398,12 +399,13 @@ ### Properties -| Name | Type | Required | Restrictions | Description | -|-------------|----------------------------------------------------------------------|----------|--------------|-------------| -| `anthropic` | [codersdk.AIBridgeAnthropicConfig](#codersdkaibridgeanthropicconfig) | false | | | -| `bedrock` | [codersdk.AIBridgeBedrockConfig](#codersdkaibridgebedrockconfig) | false | | | -| `enabled` | boolean | false | | | -| `openai` | [codersdk.AIBridgeOpenAIConfig](#codersdkaibridgeopenaiconfig) | false | | | +| Name | Type | Required | Restrictions | Description | +|--------------------------|----------------------------------------------------------------------|----------|--------------|-------------| +| `anthropic` | [codersdk.AIBridgeAnthropicConfig](#codersdkaibridgeanthropicconfig) | false | | | +| `bedrock` | [codersdk.AIBridgeBedrockConfig](#codersdkaibridgebedrockconfig) | false | | | +| `enabled` | boolean | false | | | +| `inject_coder_mcp_tools` | boolean | false | | | +| `openai` | [codersdk.AIBridgeOpenAIConfig](#codersdkaibridgeopenaiconfig) | false | | | ## codersdk.AIBridgeInterception @@ -695,6 +697,7 @@ "small_fast_model": "string" }, "enabled": true, + "inject_coder_mcp_tools": true, "openai": { "base_url": "string", "key": "string" @@ -2851,6 +2854,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "small_fast_model": "string" }, "enabled": true, + "inject_coder_mcp_tools": true, "openai": { "base_url": "string", "key": "string" @@ -3365,6 +3369,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "small_fast_model": "string" }, "enabled": true, + "inject_coder_mcp_tools": true, "openai": { "base_url": "string", "key": "string" diff --git a/docs/reference/cli/server.md b/docs/reference/cli/server.md index e689f7fa28..bcebe05e7e 100644 --- a/docs/reference/cli/server.md +++ b/docs/reference/cli/server.md @@ -1752,3 +1752,14 @@ The model to use when making requests to the AWS Bedrock API. | Default | global.anthropic.claude-haiku-4-5-20251001-v1:0 | The small fast model to use when making requests to the AWS Bedrock API. Claude Code uses Haiku-class models to perform background tasks. See https://docs.claude.com/en/docs/claude-code/settings#environment-variables. + +### --aibridge-inject-coder-mcp-tools + +| | | +|-------------|-----------------------------------------------------| +| Type | bool | +| Environment | $CODER_AIBRIDGE_INJECT_CODER_MCP_TOOLS | +| YAML | aibridge.inject_coder_mcp_tools | +| Default | false | + +Whether to inject Coder's MCP tools into intercepted AI Bridge requests (requires the "oauth2" and "mcp-server-http" experiments to be enabled). diff --git a/enterprise/aibridgedserver/aibridgedserver.go b/enterprise/aibridgedserver/aibridgedserver.go index 78939f39fb..156f3aa9d0 100644 --- a/enterprise/aibridgedserver/aibridgedserver.go +++ b/enterprise/aibridgedserver/aibridgedserver.go @@ -77,7 +77,9 @@ type Server struct { coderMCPConfig *proto.MCPServerConfig // may be nil if not available } -func NewServer(lifecycleCtx context.Context, store store, logger slog.Logger, accessURL string, externalAuthConfigs []*externalauth.Config, experiments codersdk.Experiments) (*Server, error) { +func NewServer(lifecycleCtx context.Context, store store, logger slog.Logger, accessURL string, + bridgeCfg codersdk.AIBridgeConfig, externalAuthConfigs []*externalauth.Config, experiments codersdk.Experiments, +) (*Server, error) { eac := make(map[string]*externalauth.Config, len(externalAuthConfigs)) for _, cfg := range externalAuthConfigs { @@ -88,18 +90,22 @@ func NewServer(lifecycleCtx context.Context, store store, logger slog.Logger, ac eac[cfg.ID] = cfg } - coderMCPConfig, err := getCoderMCPServerConfig(experiments, accessURL) - if err != nil { - logger.Warn(lifecycleCtx, "failed to retrieve coder MCP server config, Coder MCP will not be available", slog.Error(err)) - } - - return &Server{ + srv := &Server{ lifecycleCtx: lifecycleCtx, store: store, logger: logger.Named("aibridgedserver"), externalAuthConfigs: eac, - coderMCPConfig: coderMCPConfig, - }, nil + } + + if bridgeCfg.InjectCoderMCPTools { + coderMCPConfig, err := getCoderMCPServerConfig(experiments, accessURL) + if err != nil { + logger.Warn(lifecycleCtx, "failed to retrieve coder MCP server config, Coder MCP will not be available", slog.Error(err)) + } + srv.coderMCPConfig = coderMCPConfig + } + + return srv, nil } func (s *Server) RecordInterception(ctx context.Context, in *proto.RecordInterceptionRequest) (*proto.RecordInterceptionResponse, error) { diff --git a/enterprise/aibridgedserver/aibridgedserver_test.go b/enterprise/aibridgedserver/aibridgedserver_test.go index ff8c29d4d2..b871bfb3f8 100644 --- a/enterprise/aibridgedserver/aibridgedserver_test.go +++ b/enterprise/aibridgedserver/aibridgedserver_test.go @@ -32,6 +32,7 @@ import ( "github.com/coder/coder/v2/enterprise/aibridged/proto" "github.com/coder/coder/v2/enterprise/aibridgedserver" "github.com/coder/coder/v2/testutil" + "github.com/coder/serpent" ) var requiredExperiments = []codersdk.Experiment{ @@ -169,7 +170,7 @@ func TestAuthorization(t *testing.T) { tc.mocksFn(db, apiKey, user) } - srv, err := aibridgedserver.NewServer(t.Context(), db, logger, "/", nil, requiredExperiments) + srv, err := aibridgedserver.NewServer(t.Context(), db, logger, "/", codersdk.AIBridgeConfig{}, nil, requiredExperiments) require.NoError(t, err) require.NotNil(t, srv) @@ -203,11 +204,12 @@ func TestGetMCPServerConfigs(t *testing.T) { } cases := []struct { - name string - experiments codersdk.Experiments - externalAuthConfigs []*externalauth.Config - expectCoderMCP bool - expectedExternalMCP bool + name string + disableCoderMCPInjection bool + experiments codersdk.Experiments + externalAuthConfigs []*externalauth.Config + expectCoderMCP bool + expectedExternalMCP bool }{ { name: "experiments not enabled", @@ -238,6 +240,14 @@ func TestGetMCPServerConfigs(t *testing.T) { expectCoderMCP: true, expectedExternalMCP: true, }, + { + name: "both internal & external MCP, but coder MCP tools not injected", + disableCoderMCPInjection: true, + experiments: requiredExperiments, + externalAuthConfigs: externalAuthCfgs, + expectCoderMCP: false, + expectedExternalMCP: true, + }, } for _, tc := range cases { @@ -249,7 +259,9 @@ func TestGetMCPServerConfigs(t *testing.T) { logger := testutil.Logger(t) accessURL := "https://my-cool-deployment.com" - srv, err := aibridgedserver.NewServer(t.Context(), db, logger, accessURL, tc.externalAuthConfigs, tc.experiments) + srv, err := aibridgedserver.NewServer(t.Context(), db, logger, accessURL, codersdk.AIBridgeConfig{ + InjectCoderMCPTools: serpent.Bool(!tc.disableCoderMCPInjection), + }, tc.externalAuthConfigs, tc.experiments) require.NoError(t, err) require.NotNil(t, srv) @@ -287,7 +299,7 @@ func TestGetMCPServerAccessTokensBatch(t *testing.T) { logger := testutil.Logger(t) // Given: 2 external auth configured with MCP and 1 without. - srv, err := aibridgedserver.NewServer(t.Context(), db, logger, "/", []*externalauth.Config{ + srv, err := aibridgedserver.NewServer(t.Context(), db, logger, "/", codersdk.AIBridgeConfig{}, []*externalauth.Config{ { ID: "1", MCPURL: "1.com/mcp", @@ -794,7 +806,7 @@ func testRecordMethod[Req any, Resp any]( } ctx := testutil.Context(t, testutil.WaitLong) - srv, err := aibridgedserver.NewServer(ctx, db, logger, "/", nil, requiredExperiments) + srv, err := aibridgedserver.NewServer(ctx, db, logger, "/", codersdk.AIBridgeConfig{}, nil, requiredExperiments) require.NoError(t, err) resp, err := callMethod(srv, ctx, tc.request) diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index 492306c558..d272200609 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -82,6 +82,11 @@ OPTIONS: check is performed once per day. AIBRIDGE OPTIONS: + --aibridge-inject-coder-mcp-tools bool, $CODER_AIBRIDGE_INJECT_CODER_MCP_TOOLS (default: false) + Whether to inject Coder's MCP tools into intercepted AI Bridge + requests (requires the "oauth2" and "mcp-server-http" experiments to + be enabled). + --aibridge-anthropic-base-url string, $CODER_AIBRIDGE_ANTHROPIC_BASE_URL (default: https://api.anthropic.com/) The base URL of the Anthropic API. diff --git a/enterprise/coderd/aibridged.go b/enterprise/coderd/aibridged.go index 285575df33..2ff2de902b 100644 --- a/enterprise/coderd/aibridged.go +++ b/enterprise/coderd/aibridged.go @@ -49,7 +49,7 @@ func (api *API) CreateInMemoryAIBridgeServer(dialCtx context.Context) (client ai mux := drpcmux.New() srv, err := aibridgedserver.NewServer(api.ctx, api.Database, api.Logger.Named("aibridgedserver"), - api.AccessURL.String(), api.ExternalAuthConfigs, api.AGPL.Experiments) + api.AccessURL.String(), api.DeploymentValues.AI.BridgeConfig, api.ExternalAuthConfigs, api.AGPL.Experiments) if err != nil { return nil, err } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 42374c80af..7b4ee1820c 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -31,6 +31,7 @@ export interface AIBridgeConfig { readonly openai: AIBridgeOpenAIConfig; readonly anthropic: AIBridgeAnthropicConfig; readonly bedrock: AIBridgeBedrockConfig; + readonly inject_coder_mcp_tools: boolean; } // From codersdk/aibridge.go