diff --git a/.env.example b/.env.example index 2a21d06f..089d211a 100644 --- a/.env.example +++ b/.env.example @@ -41,6 +41,9 @@ GATEWAY_DB_PASSWORD= GATEWAY_DB_NAME=./data/mcp-gateway.db GATEWAY_DB_SSL_MODE=disable GATEWAY_STORAGE_DISK_PATH= +GATEWAY_STORAGE_API_URL= +GATEWAY_STORAGE_API_CONFIG_JSON_PATH= +GATEWAY_STORAGE_API_TIMEOUT=5 # Notifier Configuration APISERVER_NOTIFIER_ROLE=sender diff --git a/configs/mcp-gateway.yaml b/configs/mcp-gateway.yaml index 6fa9922f..573b64d8 100644 --- a/configs/mcp-gateway.yaml +++ b/configs/mcp-gateway.yaml @@ -29,6 +29,10 @@ storage: # Disk configuration (only used when type is disk) disk: path: "${GATEWAY_STORAGE_DISK_PATH:}" + api: + url: "${GATEWAY_STORAGE_API_URL:}" + configJSONPath: "${GATEWAY_STORAGE_API_CONFIG_JSON_PATH:}" + timeout: "${GATEWAY_STORAGE_API_TIMEOUT:}" # Notifier configuration notifier: diff --git a/go.mod b/go.mod index 34ccabb7..9ba2f7f9 100644 --- a/go.mod +++ b/go.mod @@ -75,7 +75,7 @@ require ( github.com/spf13/pflag v1.0.6 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect diff --git a/go.sum b/go.sum index f3bd7844..54765056 100644 --- a/go.sum +++ b/go.sum @@ -163,6 +163,8 @@ github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSW github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= diff --git a/internal/common/config/storage.go b/internal/common/config/storage.go index 85f2eed7..d101028c 100644 --- a/internal/common/config/storage.go +++ b/internal/common/config/storage.go @@ -5,9 +5,16 @@ type ( Type string `yaml:"type"` // disk or db Database DatabaseConfig `yaml:"database"` // database configuration for db type Disk DiskStorageConfig `yaml:"disk"` // disk configuration for disk type + API APIStorageConfig `yaml:"api"` // disk configuration for api type } DiskStorageConfig struct { Path string `yaml:"path"` // path for disk storage } + + APIStorageConfig struct { + Url string `yaml:"url"` // http url for api + ConfigJSONPath string `yaml:"configJSONPath"` // configJSONPath for config in http response + Timeout int `yaml:"timeout"` // timeout(seconds) for http request + } ) diff --git a/internal/mcp/storage/api.go b/internal/mcp/storage/api.go new file mode 100644 index 00000000..c3759f52 --- /dev/null +++ b/internal/mcp/storage/api.go @@ -0,0 +1,122 @@ +package storage + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "time" + + "github.com/tidwall/gjson" + + "github.com/mcp-ecosystem/mcp-gateway/internal/common/config" + + "go.uber.org/zap" +) + +// APIStore implements the Store interface using the remote http server +type APIStore struct { + logger *zap.Logger + url string + // read config from response(json body) using gjson + configJSONPath string + timeout int +} + +var _ Store = (*APIStore)(nil) + +// NewAPIStore creates a new api-based store +func NewAPIStore(logger *zap.Logger, url string, configJSONPath string, timeout int) (*APIStore, error) { + logger = logger.Named("mcp.store") + + logger.Info("Using configuration url", zap.String("path", url)) + + return &APIStore{ + logger: logger, + url: url, + configJSONPath: configJSONPath, + timeout: timeout, + }, nil +} + +// Create implements Store.Create +func (s *APIStore) Create(_ context.Context, server *config.MCPConfig) error { + // only use for read config + return nil +} + +// Get implements Store.Get +func (s *APIStore) Get(_ context.Context, name string) (*config.MCPConfig, error) { + jsonStr, err := s.request() + if err != nil { + return nil, err + } + var config config.MCPConfig + err = json.Unmarshal([]byte(jsonStr), &config) + if err != nil { + return nil, err + } + return &config, nil +} + +// List implements Store.List +func (s *APIStore) List(_ context.Context) ([]*config.MCPConfig, error) { + jsonStr, err := s.request() + if err != nil { + return nil, err + } + var configs []*config.MCPConfig + err = json.Unmarshal([]byte(jsonStr), &configs) + if err != nil { + return nil, err + } + return configs, nil +} + +// Update implements Store.Update +func (s *APIStore) Update(_ context.Context, server *config.MCPConfig) error { + // only use for read config + return nil +} + +// Delete implements Store.Delete +func (s *APIStore) Delete(_ context.Context, name string) error { + // only use for read config + return nil +} + +func (s *APIStore) request() (string, error) { + client := &http.Client{ + Timeout: time.Duration(s.timeout) * time.Second, + } + resp, err := client.Get(s.url) + if err != nil { + s.logger.Error("failed to request url", + zap.String("url", s.url), + zap.Error(err)) + return "", err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + s.logger.Error("failed to read response", + zap.String("url", s.url), + zap.Error(err)) + return "", err + } + jsonString := string(body) + s.logger.Debug("read storage api response", zap.String("body", jsonString)) + if s.configJSONPath == "" { + return jsonString, nil + } + result := gjson.Get(jsonString, s.configJSONPath) + if !result.Exists() { + err = fmt.Errorf("configJSONPath is not in response: %s", s.configJSONPath) + s.logger.Error("configJSONPath is not in response", + zap.String("url", s.url), + zap.Error(err)) + return "", err + } + return result.Raw, nil +} diff --git a/internal/mcp/storage/factory.go b/internal/mcp/storage/factory.go index dfe260ad..9884b113 100644 --- a/internal/mcp/storage/factory.go +++ b/internal/mcp/storage/factory.go @@ -3,8 +3,9 @@ package storage import ( "fmt" - "github.com/mcp-ecosystem/mcp-gateway/internal/common/config" "go.uber.org/zap" + + "github.com/mcp-ecosystem/mcp-gateway/internal/common/config" ) // NewStore creates a new store based on configuration @@ -19,6 +20,8 @@ func NewStore(logger *zap.Logger, cfg *config.StorageConfig) (Store, error) { return nil, err } return NewDBStore(logger, DatabaseType(cfg.Database.Type), dsn) + case "api": + return NewAPIStore(logger, cfg.API.Url, cfg.API.ConfigJSONPath, cfg.API.Timeout) default: return nil, fmt.Errorf("unsupported storage type: %s", cfg.Type) }