Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions internal/config/config_stdin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,9 @@ func TestConvertStdinConfig_TrustedBots(t *testing.T) {

// TestConvertStdinServerConfig_HTTPWithAuth tests that auth config is properly converted.
func TestConvertStdinServerConfig_HTTPWithAuth(t *testing.T) {
// OIDC validation now checks that ACTIONS_ID_TOKEN_REQUEST_URL is set
t.Setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "https://token.actions.example.com")

t.Run("auth with explicit audience", func(t *testing.T) {
server := &StdinServerConfig{
Type: "http",
Expand Down
11 changes: 11 additions & 0 deletions internal/config/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,17 @@ func validateAuthConfig(auth *AuthConfig, serverName, jsonPath string) error {
return rules.UnsupportedType("type", auth.Type, authPath, fmt.Sprintf("Unsupported auth type %q. Currently only \"github-oidc\" is supported", auth.Type))
}

// Fail-fast: check that required OIDC environment variables are present.
// This catches misconfigurations at config-load time rather than deferring
// the error to the first request against this server.
if os.Getenv("ACTIONS_ID_TOKEN_REQUEST_URL") == "" {
logValidateServerFailed(serverName, "ACTIONS_ID_TOKEN_REQUEST_URL is not set")
return rules.MissingRequired(
"ACTIONS_ID_TOKEN_REQUEST_URL", "github-oidc", authPath,
"Server requires OIDC authentication but ACTIONS_ID_TOKEN_REQUEST_URL is not set. "+
"OIDC auth requires running in GitHub Actions with `permissions: { id-token: write }`")
}

logValidation.Printf("Auth config validated: server=%s, type=%s", serverName, auth.Type)
return nil
}
Expand Down
21 changes: 21 additions & 0 deletions internal/config/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,7 @@ func TestValidateAuthConfig(t *testing.T) {
server *StdinServerConfig
shouldErr bool
errMsg string
clearEnv bool // when true, ensure ACTIONS_ID_TOKEN_REQUEST_URL is unset
}{
{
name: "valid github-oidc auth on http server",
Expand Down Expand Up @@ -1016,10 +1017,30 @@ func TestValidateAuthConfig(t *testing.T) {
shouldErr: true,
errMsg: "type",
},
{
name: "github-oidc rejected when ACTIONS_ID_TOKEN_REQUEST_URL is not set",
server: &StdinServerConfig{
Type: "http",
URL: "https://example.com/mcp",
Auth: &AuthConfig{
Type: "github-oidc",
Audience: "https://example.com",
},
},
shouldErr: true,
errMsg: "ACTIONS_ID_TOKEN_REQUEST_URL",
clearEnv: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.clearEnv {
t.Setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "")
} else {
// Ensure OIDC env var is set for tests that expect valid config
t.Setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "https://token.actions.example.com")
}
err := validateStandardServerConfig("test-server", tt.server, "mcpServers.test-server")
if tt.shouldErr {
require.Error(t, err)
Expand Down
12 changes: 9 additions & 3 deletions internal/launcher/launcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,19 @@ func New(ctx context.Context, cfg *config.Config) *Launcher {
logger.LogInfo("startup", "GitHub Actions OIDC provider initialized")
}

// Validate that all servers requiring OIDC have the provider available
// Defensive fallback: pre-populate serverErrors for OIDC-misconfigured servers
// so /health reports them as "error" immediately at startup. Normal config
// validation (validateAuthConfig) catches this earlier; this handles cases
// where validation was bypassed (e.g., tests constructing configs manually).
serverErrors := make(map[string]string)
for serverID, serverCfg := range cfg.Servers {
if serverCfg.Auth != nil && serverCfg.Auth.Type == "github-oidc" && oidcProvider == nil {
logger.LogError("startup",
errMsg := fmt.Sprintf(
"Server %q requires OIDC authentication but ACTIONS_ID_TOKEN_REQUEST_URL is not set. "+
"OIDC auth is only available when running in GitHub Actions with `permissions: { id-token: write }`.",
serverID)
logger.LogError("startup", "%s", errMsg)
serverErrors[serverID] = errMsg
}
}

Expand All @@ -91,7 +97,7 @@ func New(ctx context.Context, cfg *config.Config) *Launcher {
startupTimeout: startupTimeout,
oidcProvider: oidcProvider,
serverStartTimes: make(map[string]time.Time),
serverErrors: make(map[string]string),
serverErrors: serverErrors,
}
}

Expand Down
26 changes: 26 additions & 0 deletions internal/launcher/launcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,32 @@ func TestLauncher_MixedAuthTypes(t *testing.T) {
assert.Nil(t, l.oidcProvider, "OIDC provider should be nil without env vars")
}

func TestLauncher_OIDCMissingEnvRecordsErrorAtStartup(t *testing.T) {
ctx := context.Background()
t.Setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "")

cfg := &config.Config{
Servers: map[string]*config.ServerConfig{
"oidc-http": {
Type: "http",
URL: "https://secure.example.com/mcp",
Auth: &config.AuthConfig{
Type: "github-oidc",
Audience: "https://secure.example.com",
},
},
},
}

l := New(ctx, cfg)
defer l.Close()

// The launcher should record an error for the OIDC server at startup
state := l.GetServerState("oidc-http")
assert.Equal(t, "error", state.Status, "OIDC server with missing env should be in error state at startup")
assert.Contains(t, state.LastError, "ACTIONS_ID_TOKEN_REQUEST_URL")
}

func TestGetServerState_StoppedByDefault(t *testing.T) {
cfg := newTestConfig(map[string]*config.ServerConfig{
"test-server": {Type: "stdio", Command: "echo"},
Expand Down
Loading