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
6 changes: 6 additions & 0 deletions configs/mcp-gateway.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ notifier:
master_name: "${NOTIFIER_REDIS_MASTER_NAME:}" # MasterName is the sentinel master name.
username: "${NOTIFIER_REDIS_USERNAME:default}"
password: "${NOTIFIER_REDIS_PASSWORD:}"
sentinel_username: "${NOTIFIER_REDIS_SENTINEL_USERNAME:}"
sentinel_password: "${NOTIFIER_REDIS_SENTINEL_PASSWORD:}"
db: ${NOTIFIER_REDIS_DB:0}
topic: "${NOTIFIER_REDIS_TOPIC:mcp-gateway:reload}"

Expand All @@ -102,6 +104,8 @@ session:
master_name: "${SESSION_REDIS_MASTER_NAME:}" # MasterName is the sentinel master name.
username: "${SESSION_REDIS_USERNAME:default}"
password: "${SESSION_REDIS_PASSWORD:}"
sentinel_username: "${SESSION_REDIS_SENTINEL_USERNAME:}"
sentinel_password: "${SESSION_REDIS_SENTINEL_PASSWORD:}"
db: ${SESSION_REDIS_DB:0}
topic: "${SESSION_REDIS_TOPIC:mcp-gateway:session}"
prefix: "${SESSION_REDIS_PREFIX:session}"
Expand All @@ -119,6 +123,8 @@ auth:
master_name: "${OAUTH2_REDIS_MASTER_NAME:}" # MasterName is the sentinel master name.
username: "${OAUTH2_REDIS_USERNAME:default}"
password: "${OAUTH2_REDIS_PASSWORD:}"
sentinel_username: "${OAUTH2_REDIS_SENTINEL_USERNAME:}"
sentinel_password: "${OAUTH2_REDIS_SENTINEL_PASSWORD:}"
db: ${OAUTH2_REDIS_DB:0}
cors:
allowOrigins:
Expand Down
2 changes: 1 addition & 1 deletion internal/auth/storage/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func NewStore(logger *zap.Logger, cfg *config.OAuth2StorageConfig) (Store, error
case "memory":
return NewMemoryStorage(), nil
case "redis":
return NewRedisStorage(cfg.Redis.ClusterType, cfg.Redis.Addr, cfg.Redis.MasterName, cfg.Redis.Username, cfg.Redis.Password, cfg.Redis.DB)
return NewRedisStorage(cfg.Redis.ClusterType, cfg.Redis.Addr, cfg.Redis.MasterName, cfg.Redis.Username, cfg.Redis.Password, cfg.Redis.SentinelUsername, cfg.Redis.SentinelPassword, cfg.Redis.DB)
default:
return nil, fmt.Errorf("unsupported auth storage type: %s", cfg.Type)
}
Expand Down
4 changes: 3 additions & 1 deletion internal/auth/storage/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type RedisStorage struct {
}

// NewRedisStorage creates a new Redis storage instance
func NewRedisStorage(clusterType, addr, masterName string, username, password string, db int) (*RedisStorage, error) {
func NewRedisStorage(clusterType, addr, masterName string, username, password, sentinelUsername, sentinelPassword string, db int) (*RedisStorage, error) {
addrs := utils.SplitByMultipleDelimiters(addr, ";", ",")
redisOptions := &redis.UniversalOptions{
Addrs: addrs,
Expand All @@ -28,6 +28,8 @@ func NewRedisStorage(clusterType, addr, masterName string, username, password st
}
if clusterType == cnst.RedisClusterTypeSentinel {
redisOptions.MasterName = masterName
redisOptions.SentinelUsername = sentinelUsername
redisOptions.SentinelPassword = sentinelPassword
}
if clusterType != cnst.RedisClusterTypeCluster {
// can not set db in cluster mode
Expand Down
4 changes: 2 additions & 2 deletions internal/auth/storage/redis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func newTestRedisStorage(t *testing.T) (*RedisStorage, *miniredis.Miniredis) {
if err != nil {
t.Fatalf("failed to start miniredis: %v", err)
}
s, err := NewRedisStorage(cnst.RedisClusterTypeSingle, mr.Addr(), "", "", "", 0)
s, err := NewRedisStorage(cnst.RedisClusterTypeSingle, mr.Addr(), "", "", "", "", "", 0)
if err != nil {
mr.Close()
t.Fatalf("failed to create RedisStorage: %v", err)
Expand Down Expand Up @@ -101,7 +101,7 @@ func TestRedisStorage_Token_Flow(t *testing.T) {

func TestNewRedisStorage_ConnectionError(t *testing.T) {
// invalid address should fail to ping
s, err := NewRedisStorage(cnst.RedisClusterTypeSingle, "127.0.0.1:0", "", "", "", 0)
s, err := NewRedisStorage(cnst.RedisClusterTypeSingle, "127.0.0.1:0", "", "", "", "", "", 0)
assert.Nil(t, s)
assert.Error(t, err)
}
34 changes: 19 additions & 15 deletions internal/common/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,17 @@ type (

// SessionRedisConfig represents the Redis configuration for session storage
SessionRedisConfig struct {
ClusterType string `yaml:"cluster_type"` // "single", "cluster" or "sentinel"
Addr string `yaml:"addr"` // multiple addresses separated by ;.
MasterName string `yaml:"master_name"` // MasterName is the sentinel master name.
Username string `yaml:"username"`
Password string `yaml:"password"`
DB int `yaml:"db"`
Topic string `yaml:"topic"`
Prefix string `yaml:"prefix"`
TTL time.Duration `yaml:"ttl"` // TTL for session data in Redis
ClusterType string `yaml:"cluster_type"` // "single", "cluster" or "sentinel"
Addr string `yaml:"addr"` // multiple addresses separated by ;.
MasterName string `yaml:"master_name"` // MasterName is the sentinel master name.
Username string `yaml:"username"`
Password string `yaml:"password"`
SentinelUsername string `yaml:"sentinel_username"`
SentinelPassword string `yaml:"sentinel_password"`
DB int `yaml:"db"`
Topic string `yaml:"topic"`
Prefix string `yaml:"prefix"`
TTL time.Duration `yaml:"ttl"` // TTL for session data in Redis
}

// LoggerConfig represents the logger configuration
Expand Down Expand Up @@ -123,12 +125,14 @@ type (
Redis OAuth2RedisConfig `yaml:"redis"`
}
OAuth2RedisConfig struct {
ClusterType string `yaml:"cluster_type"` // "single", "cluster" or "sentinel"
Addr string `yaml:"addr"`
MasterName string `yaml:"master_name"` // MasterName is the sentinel master name.
Username string `yaml:"username"`
Password string `yaml:"password"`
DB int `yaml:"db"`
ClusterType string `yaml:"cluster_type"` // "single", "cluster" or "sentinel"
Addr string `yaml:"addr"`
MasterName string `yaml:"master_name"` // MasterName is the sentinel master name.
Username string `yaml:"username"`
Password string `yaml:"password"`
SentinelUsername string `yaml:"sentinel_username"`
SentinelPassword string `yaml:"sentinel_password"`
DB int `yaml:"db"`
}

// GoogleOAuthConfig defines Google OAuth configuration
Expand Down
43 changes: 43 additions & 0 deletions internal/common/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestResolveEnv(t *testing.T) {
Expand Down Expand Up @@ -76,3 +77,45 @@ tool_access:
assert.NoError(t, err)
assert.Equal(t, []string{"127.0.0.1/32", "localhost", "::1/128"}, []string(cfg.ToolAccess.InternalNetwork.Allowlist))
}

func TestLoadConfig_MCPGateway_RedisSentinelCredentials(t *testing.T) {
tmp := t.TempDir()
old, _ := os.Getwd()
t.Cleanup(func() { _ = os.Chdir(old) })
_ = os.Chdir(tmp)

t.Setenv("NOTIFIER_REDIS_SENTINEL_USERNAME", "sentinel-notifier")
t.Setenv("NOTIFIER_REDIS_SENTINEL_PASSWORD", "notifier-pass")
t.Setenv("SESSION_REDIS_SENTINEL_USERNAME", "sentinel-session")
t.Setenv("SESSION_REDIS_SENTINEL_PASSWORD", "session-pass")
t.Setenv("OAUTH2_REDIS_SENTINEL_USERNAME", "sentinel-oauth")
t.Setenv("OAUTH2_REDIS_SENTINEL_PASSWORD", "oauth-pass")

yaml := `
notifier:
redis:
sentinel_username: "${NOTIFIER_REDIS_SENTINEL_USERNAME:}"
sentinel_password: "${NOTIFIER_REDIS_SENTINEL_PASSWORD:}"
session:
redis:
sentinel_username: "${SESSION_REDIS_SENTINEL_USERNAME:}"
sentinel_password: "${SESSION_REDIS_SENTINEL_PASSWORD:}"
auth:
oauth2:
storage:
redis:
sentinel_username: "${OAUTH2_REDIS_SENTINEL_USERNAME:}"
sentinel_password: "${OAUTH2_REDIS_SENTINEL_PASSWORD:}"
`
file := filepath.Join(tmp, "mcp-gateway.yaml")
require.NoError(t, os.WriteFile(file, []byte(yaml), 0o644))

cfg, _, err := LoadConfig[MCPGatewayConfig]("mcp-gateway.yaml")
require.NoError(t, err)
assert.Equal(t, "sentinel-notifier", cfg.Notifier.Redis.SentinelUsername)
assert.Equal(t, "notifier-pass", cfg.Notifier.Redis.SentinelPassword)
assert.Equal(t, "sentinel-session", cfg.Session.Redis.SentinelUsername)
assert.Equal(t, "session-pass", cfg.Session.Redis.SentinelPassword)
assert.Equal(t, "sentinel-oauth", cfg.Auth.OAuth2.Storage.Redis.SentinelUsername)
assert.Equal(t, "oauth-pass", cfg.Auth.OAuth2.Storage.Redis.SentinelPassword)
}
16 changes: 9 additions & 7 deletions internal/common/config/notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ type (

// RedisConfig represents the configuration for Redis-based notifier
RedisConfig struct {
ClusterType string `yaml:"cluster_type"` // "single", "cluster" or "sentinel"
Addr string `yaml:"addr"`
MasterName string `yaml:"master_name"` // MasterName is the sentinel master name.
Username string `yaml:"username"`
Password string `yaml:"password"`
DB int `yaml:"db"`
Topic string `yaml:"topic"`
ClusterType string `yaml:"cluster_type"` // "single", "cluster" or "sentinel"
Addr string `yaml:"addr"`
MasterName string `yaml:"master_name"` // MasterName is the sentinel master name.
Username string `yaml:"username"`
Password string `yaml:"password"`
SentinelUsername string `yaml:"sentinel_username"`
SentinelPassword string `yaml:"sentinel_password"`
DB int `yaml:"db"`
Topic string `yaml:"topic"`
}
)

Expand Down
2 changes: 2 additions & 0 deletions internal/mcp/session/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ func NewRedisStore(ctx context.Context, logger *zap.Logger, cfg config.SessionRe
}
if cfg.ClusterType == cnst.RedisClusterTypeSentinel {
redisOptions.MasterName = cfg.MasterName
redisOptions.SentinelUsername = cfg.SentinelUsername
redisOptions.SentinelPassword = cfg.SentinelPassword
}
if cfg.ClusterType != cnst.RedisClusterTypeCluster {
// can not set db in cluster mode
Expand Down
4 changes: 2 additions & 2 deletions internal/mcp/storage/notifier/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func NewNotifier(ctx context.Context, logger *zap.Logger, cfg *config.NotifierCo
case TypeAPI:
return NewAPINotifier(logger, cfg.API.Port, role, cfg.API.TargetURL), nil
case TypeRedis:
return NewRedisNotifier(logger, cfg.Redis.ClusterType, cfg.Redis.Addr, cfg.Redis.MasterName, cfg.Redis.Username, cfg.Redis.Password, cfg.Redis.DB, cfg.Redis.Topic, role)
return NewRedisNotifier(logger, cfg.Redis.ClusterType, cfg.Redis.Addr, cfg.Redis.MasterName, cfg.Redis.Username, cfg.Redis.Password, cfg.Redis.SentinelUsername, cfg.Redis.SentinelPassword, cfg.Redis.DB, cfg.Redis.Topic, role)
case TypeComposite:
notifiers := make([]Notifier, 0)
// Add signal notifier
Expand All @@ -47,7 +47,7 @@ func NewNotifier(ctx context.Context, logger *zap.Logger, cfg *config.NotifierCo
notifiers = append(notifiers, apiNotifier)
// Add Redis notifier if configured
if cfg.Redis.Addr != "" {
redisNotifier, err := NewRedisNotifier(logger, cfg.Redis.ClusterType, cfg.Redis.Addr, cfg.Redis.MasterName, cfg.Redis.Username, cfg.Redis.Password, cfg.Redis.DB, cfg.Redis.Topic, role)
redisNotifier, err := NewRedisNotifier(logger, cfg.Redis.ClusterType, cfg.Redis.Addr, cfg.Redis.MasterName, cfg.Redis.Username, cfg.Redis.Password, cfg.Redis.SentinelUsername, cfg.Redis.SentinelPassword, cfg.Redis.DB, cfg.Redis.Topic, role)
if err != nil {
return nil, err
}
Expand Down
4 changes: 3 additions & 1 deletion internal/mcp/storage/notifier/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type RedisNotifier struct {
}

// NewRedisNotifier creates a new Redis-based notifier
func NewRedisNotifier(logger *zap.Logger, clusterType, addr, masterName, username, password string, db int, streamName string, role config.NotifierRole) (*RedisNotifier, error) {
func NewRedisNotifier(logger *zap.Logger, clusterType, addr, masterName, username, password, sentinelUsername, sentinelPassword string, db int, streamName string, role config.NotifierRole) (*RedisNotifier, error) {
addrs := utils.SplitByMultipleDelimiters(addr, ";", ",")
redisOptions := &redis.UniversalOptions{
Addrs: addrs,
Expand All @@ -34,6 +34,8 @@ func NewRedisNotifier(logger *zap.Logger, clusterType, addr, masterName, usernam
}
if clusterType == cnst.RedisClusterTypeSentinel {
redisOptions.MasterName = masterName
redisOptions.SentinelUsername = sentinelUsername
redisOptions.SentinelPassword = sentinelPassword
}
if clusterType != cnst.RedisClusterTypeCluster {
// can not set db in cluster mode
Expand Down
10 changes: 5 additions & 5 deletions internal/mcp/storage/notifier/redis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func TestRedisNotifier_WatchAndNotify(t *testing.T) {
logger := zap.NewNop()
stream := "unla:mcp:updates"

recv, err := NewRedisNotifier(logger, cnst.RedisClusterTypeSingle, mr.Addr(), "", "", "", 0, stream, config.RoleReceiver)
recv, err := NewRedisNotifier(logger, cnst.RedisClusterTypeSingle, mr.Addr(), "", "", "", "", "", 0, stream, config.RoleReceiver)
Comment thread
kx-byte marked this conversation as resolved.
assert.NoError(t, err)
assert.NotNil(t, recv)

Expand All @@ -48,7 +48,7 @@ func TestRedisNotifier_WatchAndNotify(t *testing.T) {
assert.NotNil(t, ch)

// Create a sender and push an update
send, err := NewRedisNotifier(logger, cnst.RedisClusterTypeSingle, mr.Addr(), "", "", "", 0, stream, config.RoleSender)
send, err := NewRedisNotifier(logger, cnst.RedisClusterTypeSingle, mr.Addr(), "", "", "", "", "", 0, stream, config.RoleSender)
assert.NoError(t, err)
cfg := &config.MCPConfig{Name: "cfg-1"}
assert.NoError(t, send.NotifyUpdate(context.Background(), cfg))
Expand Down Expand Up @@ -80,7 +80,7 @@ func TestRedisNotifier_Watch_NotReceiver(t *testing.T) {
}
defer mr.Close()

n, err := NewRedisNotifier(zap.NewNop(), cnst.RedisClusterTypeSingle, mr.Addr(), "", "", "", 0, "stream", config.RoleSender)
n, err := NewRedisNotifier(zap.NewNop(), cnst.RedisClusterTypeSingle, mr.Addr(), "", "", "", "", "", 0, "stream", config.RoleSender)
assert.NoError(t, err)
ch, werr := n.Watch(context.Background())
assert.Nil(t, ch)
Expand All @@ -94,15 +94,15 @@ func TestRedisNotifier_NotifyUpdate_NotSender(t *testing.T) {
}
defer mr.Close()

n, err := NewRedisNotifier(zap.NewNop(), cnst.RedisClusterTypeSingle, mr.Addr(), "", "", "", 0, "stream", config.RoleReceiver)
n, err := NewRedisNotifier(zap.NewNop(), cnst.RedisClusterTypeSingle, mr.Addr(), "", "", "", "", "", 0, "stream", config.RoleReceiver)
assert.NoError(t, err)
err = n.NotifyUpdate(context.Background(), &config.MCPConfig{Name: "x"})
assert.ErrorIs(t, err, cnst.ErrNotSender)
}

func TestNewRedisNotifier_ConnectionError(t *testing.T) {
// invalid address should cause ping failure
n, err := NewRedisNotifier(zap.NewNop(), cnst.RedisClusterTypeSingle, "127.0.0.1:0", "", "", "", 0, "stream", config.RoleBoth)
n, err := NewRedisNotifier(zap.NewNop(), cnst.RedisClusterTypeSingle, "127.0.0.1:0", "", "", "", "", "", 0, "stream", config.RoleBoth)
assert.Nil(t, n)
assert.Error(t, err)
}
Loading