mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
chore!: allow coder MCP tools to not be injected (#20713)
Currently, when AI Bridge is enabled AND the `oauth2` and `mcp-server-http` experiments are enabled we inject Coder's MCP tools into all intercepted AI Bridge requests. This PR introduces a config to control this behaviour. **NOTE:** this is a backwards-incompatible change; previously these tools would be injected automatically, now this setting will need to be explicitly enabled. --------- Signed-off-by: Danny Kopping <danny@coder.com>
This commit is contained in:
+5
@@ -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.
|
||||
|
||||
|
||||
+4
@@ -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
|
||||
|
||||
Generated
+3
@@ -11700,6 +11700,9 @@ const docTemplate = `{
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"inject_coder_mcp_tools": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"openai": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeOpenAIConfig"
|
||||
}
|
||||
|
||||
Generated
+3
@@ -10396,6 +10396,9 @@
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"inject_coder_mcp_tools": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"openai": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeOpenAIConfig"
|
||||
}
|
||||
|
||||
+15
-4
@@ -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 {
|
||||
|
||||
Generated
+1
@@ -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"
|
||||
|
||||
Generated
+11
-6
@@ -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"
|
||||
|
||||
Generated
+11
@@ -1752,3 +1752,14 @@ The model to use when making requests to the AWS Bedrock API.
|
||||
| Default | <code>global.anthropic.claude-haiku-4-5-20251001-v1:0</code> |
|
||||
|
||||
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 | <code>bool</code> |
|
||||
| Environment | <code>$CODER_AIBRIDGE_INJECT_CODER_MCP_TOOLS</code> |
|
||||
| YAML | <code>aibridge.inject_coder_mcp_tools</code> |
|
||||
| Default | <code>false</code> |
|
||||
|
||||
Whether to inject Coder's MCP tools into intercepted AI Bridge requests (requires the "oauth2" and "mcp-server-http" experiments to be enabled).
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Generated
+1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user