diff --git a/.golangci.yml b/.golangci.yml index 94aad9f..590397d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -90,19 +90,26 @@ linters: - whitespace settings: + + gomoddirectives: + replace-allow-list: + - github.com/sirupsen/logrus + ireturn: allow: - error - db.Error + - logic.Error mnd: ignored-files: - 'internal\\config\\.+\.go' varnamelen: + ignore-decls: + - c *gin.Context ignore-names: - up - run: issues-exit-code: 1 diff --git a/cmd/mcp_dbmem/action/direct/direct.go b/cmd/mcp_dbmem/action/direct/direct.go index 7b8a129..2affbde 100644 --- a/cmd/mcp_dbmem/action/direct/direct.go +++ b/cmd/mcp_dbmem/action/direct/direct.go @@ -21,12 +21,12 @@ import ( // Direct is the action to start the mcp server with a direct connection to the database. var Direct action.Action = func(ctx context.Context, _ []string) error { - zap.L().Info("starting pgmcp") + zap.L().Info("starting " + config.ApplicationName) // Setup tracing if viper.GetString(config.Keys.UptraceDSN) != "" { uptrace.ConfigureOpentelemetry( - uptrace.WithServiceName("mcp-dbmem"), + uptrace.WithServiceName(config.ApplicationName), uptrace.WithServiceVersion(viper.GetString(config.Keys.SoftwareVersion)), uptrace.WithDSN(viper.GetString(config.Keys.UptraceDSN)), ) @@ -70,31 +70,9 @@ var Direct action.Action = func(ctx context.Context, _ []string) error { // add tools server := mcp.NewServer(stdio.NewStdioServerTransport()) - if err := server.RegisterTool("create_entities", "Create multiple new entities in the knowledge graph", direct.CreateEntities); err != nil { - return err - } - if err := server.RegisterTool("create_relations", "Create multiple new relations between entities in the knowledge graph. Relations should be in active voice", direct.CreateRelations); err != nil { - return err - } - if err := server.RegisterTool("add_observations", "Add new observations to existing entities in the knowledge graph", direct.AddObservations); err != nil { - return err - } - if err := server.RegisterTool("delete_entities", "Delete multiple entities and their associated relations from the knowledge graph", direct.DeleteEntities); err != nil { - return err - } - if err := server.RegisterTool("delete_observations", "Delete specific observations from entities in the knowledge graph", direct.DeleteObservations); err != nil { - return err - } - if err := server.RegisterTool("delete_relations", "Delete multiple relations from the knowledge graph", direct.DeleteRelations); err != nil { - return err - } - if err := server.RegisterTool("read_graph", "Read the entire knowledge graph", direct.ReadGraph); err != nil { - return err - } - if err := server.RegisterTool("search_nodes", "Search for nodes in the knowledge graph based on a query", direct.SearchNodes); err != nil { - return err - } - if err := server.RegisterTool("open_nodes", "Open specific nodes in the knowledge graph by their names", direct.OpenNodes); err != nil { + if err := direct.Apply(server); err != nil { + zap.L().Error("Error applying direct adapter", zap.Error(err)) + return err } diff --git a/cmd/mcp_dbmem/action/server/server.go b/cmd/mcp_dbmem/action/server/server.go new file mode 100644 index 0000000..490d975 --- /dev/null +++ b/cmd/mcp_dbmem/action/server/server.go @@ -0,0 +1,117 @@ +package server + +import ( + "context" + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/spf13/viper" + "github.com/tyrm/mcp-dbmem/cmd/mcp_dbmem/action" + "github.com/tyrm/mcp-dbmem/internal/config" + "github.com/tyrm/mcp-dbmem/internal/db/bun" + "github.com/tyrm/mcp-dbmem/internal/http" + "github.com/tyrm/mcp-dbmem/internal/http/apiv1" + v1 "github.com/tyrm/mcp-dbmem/internal/logic/v1" + "github.com/uptrace/uptrace-go/uptrace" + "go.uber.org/zap" +) + +// Server is the action to start the mcp server with a direct connection to the database. +var Server action.Action = func(ctx context.Context, _ []string) error { + zap.L().Info(fmt.Sprintf("starting %s server", config.ApplicationName)) + ctx, cancel := context.WithCancel(ctx) + + // Setup tracing + if viper.GetString(config.Keys.UptraceDSN) != "" { + uptrace.ConfigureOpentelemetry( + uptrace.WithServiceName(config.ApplicationName), + uptrace.WithServiceVersion(viper.GetString(config.Keys.SoftwareVersion)), + uptrace.WithDSN(viper.GetString(config.Keys.UptraceDSN)), + ) + // Send buffered spans and free resources. + defer func() { + if err := uptrace.Shutdown(context.Background()); err != nil { + zap.L().Error("Error shutting down uptrace", zap.Error(err)) + } + }() + } + + // create database client + dbClient, err := bun.New(ctx, bun.ClientConfig{ + Type: viper.GetString(config.Keys.DBType), + Address: viper.GetString(config.Keys.DBAddress), + Port: viper.GetUint16(config.Keys.DBPort), + User: viper.GetString(config.Keys.DBUser), + Password: viper.GetString(config.Keys.DBPassword), + Database: viper.GetString(config.Keys.DBDatabase), + TLSMode: viper.GetString(config.Keys.DBTLSMode), + TLSCACert: viper.GetString(config.Keys.DBTLSCACert), + }) + if err != nil { + zap.L().Error("Error creating bun client", zap.Error(err)) + cancel() + + return err + } + defer func() { + if err := dbClient.Close(); err != nil { + zap.L().Error("Error closing bun client", zap.Error(err)) + } + }() + + // build logic + logic := v1.NewLogic(v1.LogicConfig{ + DB: dbClient, + }) + + httpServer, err := http.NewServer(ctx, http.ServerConfig{ + Logic: logic, + ApplicationName: config.ApplicationName, + HttpBind: ":4200", + }) + if err != nil { + zap.L().Error("can't start http server", zap.Error(err)) + cancel() + return err + } + // create web modules + var webModules = make([]http.Module, 0) + + zap.L().Info("loading apiv1 module") + apiV1 := apiv1.New(logic) + webModules = append(webModules, apiV1) + + // add modules to server + for _, mod := range webModules { + mod.Route(httpServer) + } + // ** start application ** + errChan := make(chan error) + + // Wait for SIGINT and SIGTERM (HIT CTRL-C) + stopSigChan := make(chan os.Signal, 1) + signal.Notify(stopSigChan, syscall.SIGINT, syscall.SIGTERM) + + // start webserver + go func(s *http.Server, errChan chan error) { + zap.L().Info("starting http server") + err := s.Start() + if err != nil { + errChan <- fmt.Errorf("http server: %s", err.Error()) + } + }(httpServer, errChan) + + // wait for event + select { + case sig := <-stopSigChan: + zap.L().Info("got signal", zap.String("signal", sig.String())) + case err := <-errChan: + zap.L().Fatal("fatal error", zap.Error(err)) + } + + cancel() + zap.L().Info("done") + return nil +} diff --git a/cmd/mcp_dbmem/flag/server.go b/cmd/mcp_dbmem/flag/server.go new file mode 100644 index 0000000..0b9bde0 --- /dev/null +++ b/cmd/mcp_dbmem/flag/server.go @@ -0,0 +1,11 @@ +package flag + +import ( + "github.com/spf13/cobra" + "github.com/tyrm/mcp-dbmem/internal/config" +) + +// Server adds flags for the server command. +func Server(cmd *cobra.Command, values config.Values) { + Database(cmd, values) +} diff --git a/cmd/mcp_dbmem/main.go b/cmd/mcp_dbmem/main.go index 38273ab..71f0595 100644 --- a/cmd/mcp_dbmem/main.go +++ b/cmd/mcp_dbmem/main.go @@ -10,6 +10,7 @@ import ( "github.com/tyrm/mcp-dbmem/cmd/mcp_dbmem/action" "github.com/tyrm/mcp-dbmem/cmd/mcp_dbmem/action/direct" "github.com/tyrm/mcp-dbmem/cmd/mcp_dbmem/action/migrate" + "github.com/tyrm/mcp-dbmem/cmd/mcp_dbmem/action/server" "github.com/tyrm/mcp-dbmem/cmd/mcp_dbmem/flag" "github.com/tyrm/mcp-dbmem/internal/config" "go.uber.org/zap" @@ -46,7 +47,7 @@ func main() { viper.Set(config.Keys.SoftwareVersion, version) rootCmd := &cobra.Command{ - Use: "mcp-dbmem", + Use: config.ApplicationName, Short: "", // TODO Version: version, SilenceErrors: true, @@ -54,7 +55,31 @@ func main() { } flag.Global(rootCmd, config.Defaults) - directCmd := &cobra.Command{ + rootCmd.AddCommand(directCommands()) + rootCmd.AddCommand(migrateCommands()) + rootCmd.AddCommand(serverCommands()) + + err = rootCmd.Execute() + if err != nil { + zap.L().Fatal("Error executing command", zap.Error(err)) + } +} + +func preRun(cmd *cobra.Command) error { + if err := config.Init(cmd.Flags()); err != nil { + return fmt.Errorf("error initializing config: %w", err) + } + + return nil +} + +func run(ctx context.Context, action action.Action, args []string) error { + return action(ctx, args) +} + +// directCommands returns the 'direct' subcommand. +func directCommands() *cobra.Command { + rootCmd := &cobra.Command{ Use: "direct", Short: "the mcp server will connect directly to the database", PreRunE: func(cmd *cobra.Command, args []string) error { @@ -64,10 +89,14 @@ func main() { return run(cmd.Context(), direct.Direct, args) }, } - flag.Direct(directCmd, config.Defaults) - rootCmd.AddCommand(directCmd) + flag.Direct(rootCmd, config.Defaults) - migrateCmd := &cobra.Command{ + return rootCmd +} + +// migrateCommands returns the 'migrate' subcommand. +func migrateCommands() *cobra.Command { + rootCmd := &cobra.Command{ Use: "migrate", Short: "run db migrations", PreRunE: func(cmd *cobra.Command, _ []string) error { @@ -77,23 +106,24 @@ func main() { return run(cmd.Context(), migrate.Migrate, args) }, } - flag.Migrate(migrateCmd, config.Defaults) - rootCmd.AddCommand(migrateCmd) + flag.Migrate(rootCmd, config.Defaults) - err = rootCmd.Execute() - if err != nil { - zap.L().Fatal("Error executing command", zap.Error(err)) - } + return rootCmd } -func preRun(cmd *cobra.Command) error { - if err := config.Init(cmd.Flags()); err != nil { - return fmt.Errorf("error initializing config: %w", err) +// serverCommands returns the 'server' subcommand. +func serverCommands() *cobra.Command { + serverCmd := &cobra.Command{ + Use: "server", + Short: "start the server", + PreRunE: func(cmd *cobra.Command, args []string) error { + return preRun(cmd) + }, + RunE: func(cmd *cobra.Command, args []string) error { + return run(cmd.Context(), server.Server, args) + }, } + flag.Server(serverCmd, config.Defaults) - return nil -} - -func run(ctx context.Context, action action.Action, args []string) error { - return action(ctx, args) + return serverCmd } diff --git a/go.mod b/go.mod index e1bad71..61782a8 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,13 @@ module github.com/tyrm/mcp-dbmem go 1.24 replace ( - // Pinning to v1.10.0 to address a vulnerability in the gin-gonic/gin package. - github.com/gin-gonic/gin => github.com/gin-gonic/gin v1.10.0 - // Pinning to v1.9.3 to address a vulnerability in the github.com/jackc/pgx. - github.com/jackc/pgx/v4 => github.com/jackc/pgx/v4 v4.18.3 // Pinning to v1.9.3 to address a vulnerability in the github.com/sirupsen/logrus package. github.com/sirupsen/logrus => github.com/sirupsen/logrus v1.9.3 ) require ( + github.com/gin-contrib/sessions v1.0.3 + github.com/gin-gonic/gin v1.10.1 github.com/go-sql-driver/mysql v1.9.2 github.com/jackc/pgconn v1.14.3 github.com/jackc/pgx/v4 v4.18.3 @@ -26,6 +24,7 @@ require ( github.com/uptrace/bun/dialect/sqlitedialect v1.2.11 github.com/uptrace/bun/extra/bunotel v1.2.11 github.com/uptrace/uptrace-go v1.35.1 + go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0 go.opentelemetry.io/otel v1.35.0 go.opentelemetry.io/otel/trace v1.35.0 go.uber.org/zap v1.27.0 @@ -37,14 +36,26 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect + github.com/bytedance/sonic v1.13.2 // indirect + github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.0.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/context v1.1.2 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect + github.com/gorilla/sessions v1.4.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/jsonschema v0.13.0 // indirect @@ -55,8 +66,13 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgtype v1.14.4 // indirect github.com/jinzhu/inflection v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -73,6 +89,8 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect @@ -91,6 +109,7 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect + golang.org/x/arch v0.16.0 // indirect golang.org/x/crypto v0.38.0 // indirect golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect golang.org/x/mod v0.24.0 // indirect diff --git a/go.sum b/go.sum index 278e6d5..10592d1 100644 --- a/go.sum +++ b/go.sum @@ -7,8 +7,16 @@ github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPn github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= +github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= +github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -24,6 +32,14 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/sessions v1.0.3 h1:AZ4j0AalLsGqdrKNbbrKcXx9OJZqViirvNGsJTxcQps= +github.com/gin-contrib/sessions v1.0.3/go.mod h1:5i4XMx4KPtQihnzxEqG9u1K446lO3G19jAi2GtbfsAI= +github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= +github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= +github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= +github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -31,22 +47,41 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= +github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= +github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= +github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -106,7 +141,13 @@ github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -116,11 +157,14 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= @@ -132,6 +176,11 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/metoro-io/mcp-golang v0.12.0 h1:CFfESIXD9trCNnMFhLL5XXgC4X0EhVbZZ7kfv+5xgkg= github.com/metoro-io/mcp-golang v0.12.0/go.mod h1:ifLP9ZzKpN1UqFWNTpAHOqSvNkMK6b7d1FSZ5Lu0lN0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -175,6 +224,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= @@ -182,6 +232,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -198,6 +249,10 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/uptrace/bun v1.2.11 h1:l9dTymsdZZAoSZ1+Qo3utms0RffgkDbIv+1UGk8N1wQ= github.com/uptrace/bun v1.2.11/go.mod h1:ww5G8h59UrOnCHmZ8O1I/4Djc7M/Z3E+EWFS2KLB6dQ= github.com/uptrace/bun/dialect/mysqldialect v1.2.11 h1:4WLBf2ajPv62PaiPj7nbWF0J+kcHK6+Ro1Fg152iIKA= @@ -222,6 +277,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0 h1:jj/B7eX95/mOxim9g9laNZkOHKz/XCHG0G410SntRy4= +go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0/go.mod h1:ZvRTVaYYGypytG0zRp2A60lpj//cMq3ZnxYdZaljVBM= go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0 h1:0NgN/3SYkqYJ9NBlDfl/2lzVlwos/YQLvi8sUrzJRBE= go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0/go.mod h1:oxpUfhTkhgQaYIjtBt3T3w135dLoxq//qo3WPlPIKkE= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= @@ -267,6 +324,8 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U= +golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -403,5 +462,6 @@ modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= tyr.codes/libs/libmigration v0.5.1 h1:1LX80UTSZF0n2EHwW4jV1rYMMOPuMDhmJKT0lTPaVB0= tyr.codes/libs/libmigration v0.5.1/go.mod h1:9So6/trcbQ+jTzqnLHyhBgqFPTSTvbT6/gH0yMU7tvQ= diff --git a/internal/adapter/adapter.go b/internal/adapter/adapter.go index 4541288..e6cc683 100644 --- a/internal/adapter/adapter.go +++ b/internal/adapter/adapter.go @@ -6,6 +6,12 @@ import ( mcp "github.com/metoro-io/mcp-golang" ) +var ( + RespEntityDeleted = mcp.NewToolResponse(mcp.NewTextContent("Entities deleted successfully")) + RespObservationDeleted = mcp.NewToolResponse(mcp.NewTextContent("Observations deleted successfully")) + RespRelationDeleted = mcp.NewToolResponse(mcp.NewTextContent("Relations deleted successfully")) +) + type Adapter interface { CreateEntities(ctx context.Context, args CreateEntitiesArgs) (*mcp.ToolResponse, error) DeleteEntities(ctx context.Context, args DeleteEntitiesArgs) (*mcp.ToolResponse, error) @@ -19,32 +25,32 @@ type Adapter interface { Apply(server *mcp.Server) error } -func apply[A Adapter](a A, server *mcp.Server) error { - if err := server.RegisterTool("create_entities", "Create multiple new entities in the knowledge graph", a.CreateEntities); err != nil { +func apply[A Adapter](adapter A, server *mcp.Server) error { + if err := server.RegisterTool("create_entities", "Create multiple new entities in the knowledge graph", adapter.CreateEntities); err != nil { return err } - if err := server.RegisterTool("create_relations", "Create multiple new relations between entities in the knowledge graph. Relations should be in active voice", a.CreateRelations); err != nil { + if err := server.RegisterTool("create_relations", "Create multiple new relations between entities in the knowledge graph. Relations should be in active voice", adapter.CreateRelations); err != nil { return err } - if err := server.RegisterTool("add_observations", "Add new observations to existing entities in the knowledge graph", a.AddObservations); err != nil { + if err := server.RegisterTool("add_observations", "Add new observations to existing entities in the knowledge graph", adapter.AddObservations); err != nil { return err } - if err := server.RegisterTool("delete_entities", "Delete multiple entities and their associated relations from the knowledge graph", a.DeleteEntities); err != nil { + if err := server.RegisterTool("delete_entities", "Delete multiple entities and their associated relations from the knowledge graph", adapter.DeleteEntities); err != nil { return err } - if err := server.RegisterTool("delete_observations", "Delete specific observations from entities in the knowledge graph", a.DeleteObservations); err != nil { + if err := server.RegisterTool("delete_observations", "Delete specific observations from entities in the knowledge graph", adapter.DeleteObservations); err != nil { return err } - if err := server.RegisterTool("delete_relations", "Delete multiple relations from the knowledge graph", a.DeleteRelations); err != nil { + if err := server.RegisterTool("delete_relations", "Delete multiple relations from the knowledge graph", adapter.DeleteRelations); err != nil { return err } - if err := server.RegisterTool("read_graph", "Read the entire knowledge graph", a.ReadGraph); err != nil { + if err := server.RegisterTool("read_graph", "Read the entire knowledge graph", adapter.ReadGraph); err != nil { return err } - if err := server.RegisterTool("search_nodes", "Search for nodes in the knowledge graph based on a query", a.SearchNodes); err != nil { + if err := server.RegisterTool("search_nodes", "Search for nodes in the knowledge graph based on adapter query", adapter.SearchNodes); err != nil { return err } - if err := server.RegisterTool("open_nodes", "Open specific nodes in the knowledge graph by their names", a.OpenNodes); err != nil { + if err := server.RegisterTool("open_nodes", "Open specific nodes in the knowledge graph by their names", adapter.OpenNodes); err != nil { return err } diff --git a/internal/adapter/common.go b/internal/adapter/common.go deleted file mode 100644 index b8e8da3..0000000 --- a/internal/adapter/common.go +++ /dev/null @@ -1 +0,0 @@ -package adapter diff --git a/internal/adapter/direct.go b/internal/adapter/direct.go index 869799a..cdfa48a 100644 --- a/internal/adapter/direct.go +++ b/internal/adapter/direct.go @@ -22,16 +22,16 @@ type DirectAdapter struct { logic logic.Logic } -func (d *DirectAdapter) Apply(server *mcp.Server) error { - return apply(d, server) -} - func NewDirectAdapter(logic logic.Logic) *DirectAdapter { return &DirectAdapter{ logic: logic, } } +func (d *DirectAdapter) Apply(server *mcp.Server) error { + return apply(d, server) +} + func (d *DirectAdapter) CreateEntities(ctx context.Context, args CreateEntitiesArgs) (*mcp.ToolResponse, error) { ctx, span := directTracer.Start(ctx, "CreateEntities", directTracerAttrs...) defer span.End() @@ -99,11 +99,11 @@ func (d *DirectAdapter) DeleteEntities(ctx context.Context, args DeleteEntitiesA continue } - if err := d.logic.DeleteAllObservationsByEntityID(ctx, entity.ID); err != nil { - zap.L().Error("Can't delete observations", zap.Error(err), zap.String("entityName", entityName)) - span.RecordError(err) - return nil, err - } + //if err := d.logic.DeleteAllObservationsByEntityID(ctx, entity.ID); err != nil { + // zap.L().Error("Can't delete observations", zap.Error(err), zap.String("entityName", entityName)) + // span.RecordError(err) + // return nil, err + //} if err := d.logic.DeleteEntity(ctx, entity); err != nil { zap.L().Error("Can't delete entity", zap.Error(err), zap.String("entityName", entityName)) @@ -112,9 +112,7 @@ func (d *DirectAdapter) DeleteEntities(ctx context.Context, args DeleteEntitiesA } } - return mcp.NewToolResponse( - mcp.NewTextContent("Entities deleted successfully"), - ), nil + return RespEntityDeleted, nil } func (d *DirectAdapter) ReadGraph(ctx context.Context, _ ReadGraphArgs) (*mcp.ToolResponse, error) { @@ -286,9 +284,7 @@ func (d *DirectAdapter) DeleteObservations(ctx context.Context, args DeleteObser } } - return mcp.NewToolResponse( - mcp.NewTextContent("Observations deleted successfully"), - ), nil + return RespObservationDeleted, nil } func (d *DirectAdapter) CreateRelations(ctx context.Context, args CreateRelationsArgs) (*mcp.ToolResponse, error) { @@ -334,7 +330,6 @@ func (d *DirectAdapter) CreateRelations(ctx context.Context, args CreateRelation } return toolResponse, err - } func (d *DirectAdapter) DeleteRelations(ctx context.Context, args DeleteRelationsArgs) (*mcp.ToolResponse, error) { @@ -383,9 +378,7 @@ func (d *DirectAdapter) DeleteRelations(ctx context.Context, args DeleteRelation } } - return mcp.NewToolResponse( - mcp.NewTextContent("Relations deleted successfully"), - ), nil + return RespRelationDeleted, nil } var _ Adapter = (*DirectAdapter)(nil) diff --git a/internal/db/bun/entity.go b/internal/db/bun/entity.go index 5a478bc..3494e62 100644 --- a/internal/db/bun/entity.go +++ b/internal/db/bun/entity.go @@ -2,6 +2,7 @@ package bun import ( "context" + "database/sql" "errors" "github.com/tyrm/mcp-dbmem/internal/db" @@ -22,9 +23,47 @@ func (c *Client) DeleteEntity(ctx context.Context, entity *models.Entity) db.Err ctx, span := tracer.Start(ctx, "DeleteEntity", tracerAttrs...) defer span.End() - err := c.delete(ctx, entity) - span.RecordError(err) - return err + tx, err := c.db.BeginTx(ctx, &sql.TxOptions{}) + if err != nil { + return c.ProcessError(err) + } + + if err := deleteAllRelationsByEntityID(ctx, tx, entity.ID); err != nil { + span.RecordError(err) + if err := tx.Rollback(); err != nil { + span.RecordError(err) + return c.ProcessError(err) + } + return c.ProcessError(err) + } + + if err := deleteAllObservationsByEntityID(ctx, tx, entity.ID); err != nil { + span.RecordError(err) + if err := tx.Rollback(); err != nil { + span.RecordError(err) + return c.ProcessError(err) + } + return c.ProcessError(err) + } + + query := tx.NewDelete(). + Model(entity). + WherePK() + if _, err := query.Exec(ctx); err != nil { + span.RecordError(err) + if err := tx.Rollback(); err != nil { + span.RecordError(err) + return c.ProcessError(err) + } + return c.ProcessError(err) + } + + if err := tx.Commit(); err != nil { + span.RecordError(err) + return c.ProcessError(err) + } + + return nil } func (c *Client) ReadAllEntities(ctx context.Context) ([]*models.Entity, db.Error) { diff --git a/internal/db/bun/observation.go b/internal/db/bun/observation.go index 6e32691..7eaa35c 100644 --- a/internal/db/bun/observation.go +++ b/internal/db/bun/observation.go @@ -18,22 +18,6 @@ func (c *Client) CreateObservation(ctx context.Context, observation *models.Obse return err } -func (c *Client) DeleteAllObservationsByEntityID(ctx context.Context, entityID int64) db.Error { - ctx, span := tracer.Start(ctx, "DeleteAllObservationsByEntityID", tracerAttrs...) - defer span.End() - - query := c.db.NewDelete(). - Model((*models.Observation)(nil)). - Where("entity_id = ?", entityID) - - if _, err := query.Exec(ctx); err != nil { - span.RecordError(err) - return c.ProcessError(err) - } - - return nil -} - func (c *Client) DeleteObservation(ctx context.Context, observation *models.Observation) db.Error { ctx, span := tracer.Start(ctx, "DeleteObservation", tracerAttrs...) defer span.End() @@ -68,3 +52,19 @@ func newObservationQ(c bun.IDB, i *models.Observation) *bun.SelectQuery { NewSelect(). Model(i) } + +func deleteAllObservationsByEntityID(ctx context.Context, c bun.IDB, entityID int64) db.Error { + ctx, span := tracer.Start(ctx, "deleteAllObservationsByEntityID", tracerAttrs...) + defer span.End() + + query := c.NewDelete(). + Model((*models.Observation)(nil)). + Where("entity_id = ?", entityID) + + if _, err := query.Exec(ctx); err != nil { + span.RecordError(err) + return err + } + + return nil +} diff --git a/internal/db/bun/relation.go b/internal/db/bun/relation.go index 9a7e065..ff8bae9 100644 --- a/internal/db/bun/relation.go +++ b/internal/db/bun/relation.go @@ -18,24 +18,6 @@ func (c *Client) CreateRelation(ctx context.Context, relation *models.Relation) return err } -func (c *Client) DeleteAllRelationsByEntityID(ctx context.Context, entityID int64) db.Error { - ctx, span := tracer.Start(ctx, "DeleteAllRelationsByEntityID", tracerAttrs...) - defer span.End() - - query := c.db. - NewDelete(). - Model((*models.Relation)(nil)). - Where("from_id = ?", entityID). - WhereOr("to_id = ?", entityID) - - if _, err := query.Exec(ctx); err != nil { - span.RecordError(err) - return c.ProcessError(err) - } - - return nil -} - func (c *Client) DeleteRelation(ctx context.Context, relation *models.Relation) db.Error { ctx, span := tracer.Start(ctx, "DeleteRelation", tracerAttrs...) defer span.End() @@ -81,6 +63,23 @@ func (c *Client) ReadExactRelation(ctx context.Context, fromID, toID int64, rela return relation, nil } +func deleteAllRelationsByEntityID(ctx context.Context, c bun.IDB, entityID int64) db.Error { + ctx, span := tracer.Start(ctx, "deleteAllRelationsByEntityID", tracerAttrs...) + defer span.End() + + query := c. + NewDelete(). + Model((*models.Relation)(nil)). + Where("from_id = ?", entityID). + WhereOr("to_id = ?", entityID) + + if _, err := query.Exec(ctx); err != nil { + span.RecordError(err) + return err + } + + return nil +} func newRelationQ(c bun.IDB, i *models.Relation) *bun.SelectQuery { return c. NewSelect(). diff --git a/internal/db/db.go b/internal/db/db.go index a55fac9..b90d56c 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -13,24 +13,40 @@ type DB interface { Relations } +// Entities is the interface that wraps the basic entity operations. type Entities interface { + // CreateEntity creates a new entity and observations in the database. CreateEntity(ctx context.Context, entity *models.Entity) Error + // DeleteEntity deletes an entity and all its associated observations and relations. DeleteEntity(ctx context.Context, entity *models.Entity) Error + // ReadAllEntities reads all entities and their observations from the database. ReadAllEntities(ctx context.Context) ([]*models.Entity, Error) + // ReadEntityByName reads an entity by its name and returns the entity and its observations. ReadEntityByName(ctx context.Context, name string) (*models.Entity, Error) } +// Observations is the interface that wraps the basic observation operations. type Observations interface { + // CreateObservation creates a new observation in the database. CreateObservation(ctx context.Context, observation *models.Observation) Error - DeleteAllObservationsByEntityID(ctx context.Context, entityID int64) Error + //DeleteAllObservationsByEntityID(ctx context.Context, entityID int64) Error + + // DeleteObservation deletes an observation from the database. DeleteObservation(ctx context.Context, observation *models.Observation) Error + // ReadObservationByTextForEntityID reads an observation by its text and entity ID. ReadObservationByTextForEntityID(ctx context.Context, entityID int64, text string) (*models.Observation, Error) } +// Relations is the interface that wraps the basic relation operations. type Relations interface { + // CreateRelation creates a new relation in the database. CreateRelation(ctx context.Context, relation *models.Relation) Error - DeleteAllRelationsByEntityID(ctx context.Context, entityID int64) Error + //DeleteAllRelationsByEntityID(ctx context.Context, entityID int64) Error + + // ReadAllRelations reads all relations from the database. ReadAllRelations(ctx context.Context) ([]*models.Relation, Error) + // ReadExactRelation reads a relation by its from entity, to entity, and relation type. ReadExactRelation(ctx context.Context, fromID, toID int64, relationType string) (*models.Relation, Error) + // DeleteRelation deletes a relation from the database. DeleteRelation(ctx context.Context, relation *models.Relation) Error } diff --git a/internal/http/apiv1/api.go b/internal/http/apiv1/api.go new file mode 100644 index 0000000..613ba44 --- /dev/null +++ b/internal/http/apiv1/api.go @@ -0,0 +1,39 @@ +package apiv1 + +import ( + "github.com/go-playground/validator/v10" + ihttp "github.com/tyrm/mcp-dbmem/internal/http" + "github.com/tyrm/mcp-dbmem/internal/http/path" + "github.com/tyrm/mcp-dbmem/internal/logic" +) + +type API struct { + logic logic.Logic + validate *validator.Validate +} + +func New(logic logic.Logic) *API { + return &API{ + logic: logic, + validate: validator.New(validator.WithRequiredStructEnabled()), + } +} + +func (a *API) Name() string { + return "apiv1" +} + +// Route attaches routes to the web server. +func (a *API) Route(s *ihttp.Server) { + api := s.Group(path.V1) + + api.POST(path.Entities, a.entityPOST) + api.DELETE(path.Entities, a.entityDELETE) + api.POST(path.Graph, a.graphGET) + api.GET(path.Nodes, a.nodeGET) + api.GET(path.NodesSearch, a.nodeSearchGET) + api.POST(path.Observations, a.observationPOST) + api.DELETE(path.Observations, a.observationDELETE) + api.POST(path.Relations, a.relationPOST) + api.DELETE(path.Relations, a.relationDELETE) +} diff --git a/internal/http/apiv1/entity.go b/internal/http/apiv1/entity.go new file mode 100644 index 0000000..d81996d --- /dev/null +++ b/internal/http/apiv1/entity.go @@ -0,0 +1,45 @@ +package apiv1 + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/tyrm/mcp-dbmem/pkg/api" +) + +func (a *API) entityPOST(c *gin.Context) { + ctx, span := tracer.Start(c, "entityPOST", tracerAttrs...) + defer span.End() + + var entity api.CreateEntitiesRequest + // Bind the JSON body to your struct + if err := c.ShouldBindJSON(&entity); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Validate the request body + if err := a.validate.StructCtx(ctx, entity); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // create entity models + entities := make([]api.Entity, len(entity.Entities)) + for i, e := range entity.Entities { + entities[i] = api.Entity{ + Name: e.Name, + Type: e.Type, + Observations: e.Observations, + } + } + + c.JSON(http.StatusNotImplemented, struct{}{}) +} + +func (a *API) entityDELETE(c *gin.Context) { + _, span := tracer.Start(c, "entityDELETE", tracerAttrs...) + defer span.End() + + c.JSON(http.StatusNotImplemented, struct{}{}) +} diff --git a/internal/http/apiv1/graph.go b/internal/http/apiv1/graph.go new file mode 100644 index 0000000..dba94e2 --- /dev/null +++ b/internal/http/apiv1/graph.go @@ -0,0 +1,8 @@ +package apiv1 + +import "github.com/gin-gonic/gin" + +func (a *API) graphGET(c *gin.Context) { + _, span := tracer.Start(c, "graphGET", tracerAttrs...) + defer span.End() +} diff --git a/internal/http/apiv1/node.go b/internal/http/apiv1/node.go new file mode 100644 index 0000000..3aa1d32 --- /dev/null +++ b/internal/http/apiv1/node.go @@ -0,0 +1,13 @@ +package apiv1 + +import "github.com/gin-gonic/gin" + +func (a *API) nodeGET(c *gin.Context) { + _, span := tracer.Start(c, "nodeGET", tracerAttrs...) + defer span.End() +} + +func (a *API) nodeSearchGET(c *gin.Context) { + _, span := tracer.Start(c, "nodeSearchGET", tracerAttrs...) + defer span.End() +} diff --git a/internal/http/apiv1/observation.go b/internal/http/apiv1/observation.go new file mode 100644 index 0000000..2810b9b --- /dev/null +++ b/internal/http/apiv1/observation.go @@ -0,0 +1,13 @@ +package apiv1 + +import "github.com/gin-gonic/gin" + +func (a *API) observationPOST(c *gin.Context) { + _, span := tracer.Start(c, "observationPOST", tracerAttrs...) + defer span.End() +} + +func (a *API) observationDELETE(c *gin.Context) { + _, span := tracer.Start(c, "observationDELETE", tracerAttrs...) + defer span.End() +} diff --git a/internal/http/apiv1/relation.go b/internal/http/apiv1/relation.go new file mode 100644 index 0000000..6d8edae --- /dev/null +++ b/internal/http/apiv1/relation.go @@ -0,0 +1,13 @@ +package apiv1 + +import "github.com/gin-gonic/gin" + +func (a *API) relationPOST(c *gin.Context) { + _, span := tracer.Start(c, "relationPOST", tracerAttrs...) + defer span.End() +} + +func (a *API) relationDELETE(c *gin.Context) { + _, span := tracer.Start(c, "relationDELETE", tracerAttrs...) + defer span.End() +} diff --git a/internal/http/apiv1/trace.go b/internal/http/apiv1/trace.go new file mode 100644 index 0000000..454a034 --- /dev/null +++ b/internal/http/apiv1/trace.go @@ -0,0 +1,9 @@ +package apiv1 + +import ( + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" +) + +var tracer = otel.Tracer("internal/http/apiv1") +var tracerAttrs = []trace.SpanStartOption{} diff --git a/internal/http/module.go b/internal/http/module.go new file mode 100644 index 0000000..48bdc21 --- /dev/null +++ b/internal/http/module.go @@ -0,0 +1,7 @@ +package http + +// Module represents a module that can be added to a http server. +type Module interface { + Name() string + Route(s *Server) +} diff --git a/internal/http/path/paths.go b/internal/http/path/paths.go new file mode 100644 index 0000000..cba5440 --- /dev/null +++ b/internal/http/path/paths.go @@ -0,0 +1,13 @@ +package path + +const ( + Base = "/api" + V1 = Base + "/v1" + + Entities = "/entities" + Graph = "/graph" + Observations = "/observations" + Relations = "/relations" + Nodes = "/nodes" + NodesSearch = Nodes + "/search" +) diff --git a/internal/http/server.go b/internal/http/server.go new file mode 100644 index 0000000..b650c9d --- /dev/null +++ b/internal/http/server.go @@ -0,0 +1,100 @@ +package http + +import ( + "context" + "net/http" + "time" + + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "github.com/tyrm/mcp-dbmem/internal/logic" + "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" +) + +const serverTimeout = 5 * time.Second + +type Server struct { + engine *gin.Engine + logic logic.Logic + srv *http.Server +} + +type ServerConfig struct { + Logic logic.Logic + Store sessions.Store + ApplicationName string + HttpBind string +} + +// NewServer creates a new http web server. +func NewServer(_ context.Context, cnf ServerConfig) (*Server, error) { + engine := gin.Default() + + engine.Use(otelgin.Middleware(cnf.ApplicationName)) + engine.Use(sessions.Sessions(cnf.ApplicationName, cnf.Store)) + + return &Server{ + engine: engine, + logic: cnf.Logic, + srv: &http.Server{ + Addr: cnf.HttpBind, + Handler: engine, + WriteTimeout: serverTimeout, + ReadTimeout: serverTimeout, + }, + }, nil +} + +func (s *Server) Group(relativePath string, handlers ...gin.HandlerFunc) *gin.RouterGroup { + return s.engine.Group(relativePath, handlers...) +} + +func (s *Server) Static(relativePath string, root string) { + s.engine.Static(relativePath, root) +} + +func (s *Server) StaticFS(relativePath string, fs http.FileSystem) { + s.engine.StaticFS(relativePath, fs) +} + +// Start starts the web server. +func (s *Server) Start() error { + return s.srv.ListenAndServe() +} + +// Stop shuts down the web server. +func (s *Server) Stop(ctx context.Context) error { + return s.srv.Shutdown(ctx) +} + +func (s *Server) Use(handlers ...gin.HandlerFunc) { + s.engine.Use(handlers...) +} + +func (s *Server) Delete(relativePath string, handlers ...gin.HandlerFunc) { + s.engine.DELETE(relativePath, handlers...) +} + +func (s *Server) Get(relativePath string, handlers ...gin.HandlerFunc) { + s.engine.GET(relativePath, handlers...) +} + +func (s *Server) Head(relativePath string, handlers ...gin.HandlerFunc) { + s.engine.HEAD(relativePath, handlers...) +} + +func (s *Server) Options(relativePath string, handlers ...gin.HandlerFunc) { + s.engine.OPTIONS(relativePath, handlers...) +} + +func (s *Server) Path(relativePath string, handlers ...gin.HandlerFunc) { + s.engine.PATCH(relativePath, handlers...) +} + +func (s *Server) Post(relativePath string, handlers ...gin.HandlerFunc) { + s.engine.POST(relativePath, handlers...) +} + +func (s *Server) Put(relativePath string, handlers ...gin.HandlerFunc) { + s.engine.PUT(relativePath, handlers...) +} diff --git a/internal/logic/logic.go b/internal/logic/logic.go index 0ff08dd..7177ea6 100644 --- a/internal/logic/logic.go +++ b/internal/logic/logic.go @@ -15,24 +15,40 @@ type Logic interface { Relations } +// Entities is the interface that wraps the basic entity operations. type Entities interface { + // CreateEntity creates a new entity and observations. CreateEntity(ctx context.Context, entity *models.Entity) error + // DeleteEntity deletes an entity and all its associated observations and relations. DeleteEntity(ctx context.Context, entity *models.Entity) error + // ReadAllEntities reads all entities and their observations. ReadAllEntities(ctx context.Context) ([]*models.Entity, error) + // ReadEntityByName reads an entity by its name and returns the entity and its observations. ReadEntityByName(ctx context.Context, name string) (*models.Entity, error) } +// Observations is the interface that wraps the basic observation operations. type Observations interface { + // CreateObservation creates a new observation. CreateObservation(ctx context.Context, observation *models.Observation) error - DeleteAllObservationsByEntityID(ctx context.Context, entityID int64) error + //DeleteAllObservationsByEntityID(ctx context.Context, entityID int64) error + + // DeleteObservation deletes an observation. DeleteObservation(ctx context.Context, observation *models.Observation) error + // ReadObservationByTextForEntityID reads an observation by its text and entity ID. ReadObservationByTextForEntityID(ctx context.Context, entityID int64, text string) (*models.Observation, error) } +// Relations is the interface that wraps the basic relation operations. type Relations interface { + // CreateRelation creates a new relation. CreateRelation(ctx context.Context, relation *models.Relation) error - DeleteAllRelationsByEntityID(ctx context.Context, entityID int64) error + //DeleteAllRelationsByEntityID(ctx context.Context, entityID int64) error + + // ReadAllRelations reads all relations. ReadAllRelations(ctx context.Context) ([]*models.Relation, error) + // ReadExactRelation reads a relation by its from entity, to entity, and relation type. ReadExactRelation(ctx context.Context, fromID, toID int64, relationType string) (*models.Relation, error) + // DeleteRelation deletes a relation. DeleteRelation(ctx context.Context, relation *models.Relation) error } diff --git a/internal/logic/v1/entity.go b/internal/logic/v1/entity.go deleted file mode 100644 index a0279b6..0000000 --- a/internal/logic/v1/entity.go +++ /dev/null @@ -1,105 +0,0 @@ -package v1 - -//// Entity represents an entity in the knowledge graph. -//type Entity struct { -// Name string `json:"name" jsonschema:"required,description=The name of the entity"` -// Type string `json:"entityType" jsonschema:"required,description=The type of the entity"` -// Observations []string `json:"observations" jsonschema:"required,description=An array of observation contents associated with the entity"` -//} -// -//// CreateEntitiesArgs represents the arguments for creating entities. -//type CreateEntitiesArgs struct { -// Entities []Entity `json:"entities" jsonschema:"required,description=An array of observation contents associated with the entity"` -//} -// -//// CreateEntities creates entities in the knowledge graph. -//func (l *Logic) CreateEntities(ctx context.Context, args CreateEntitiesArgs) (*mcp.ToolResponse, error) { -// ctx, span := tracer.Start(ctx, "CreateEntities", tracerAttrs...) -// defer span.End() -// -// response := make([]Entity, 0) -// for _, entity := range args.Entities { -// // Process each entity -// newEntity := &models.Entity{ -// Name: entity.Name, -// Type: entity.Type, -// } -// if err := l.DB.CreateEntity(ctx, newEntity); err != nil { -// zap.L().Error("Can't create entity from database", zap.Error(err), zap.Any("entity", newEntity)) -// span.RecordError(err) -// return nil, err -// } -// newEntityResponse := Entity{ -// Name: newEntity.Name, -// Type: newEntity.Type, -// Observations: make([]string, 0), -// } -// -// for _, observation := range entity.Observations { -// newObservation := &models.Observation{ -// EntityID: newEntity.ID, -// Contents: observation, -// } -// if err := l.DB.CreateObservation(ctx, newObservation); err != nil { -// zap.L().Error("Can't create observation in database", zap.Error(err), zap.Any("observation", newObservation)) -// span.RecordError(err) -// return nil, err -// } -// -// newEntityResponse.Observations = append(newEntityResponse.Observations, newObservation.Contents) -// } -// -// response = append(response, newEntityResponse) -// } -// -// // convert response to json string -// toolResponse, err := toolJSONResponse(ctx, response) -// if err != nil { -// zap.L().Error("Can't marshal response json", zap.Error(err)) -// span.RecordError(err) -// return nil, err -// } -// -// return toolResponse, nil -//} -// -//// DeleteEntitiesArgs represents the arguments for deleting entities. -//type DeleteEntitiesArgs struct { -// EntityNames []string `json:"entityNames" jsonschema:"required,description=An array of entity names to delete"` -//} -// -//// DeleteEntities deletes entities from the knowledge graph. -//func (l *Logic) DeleteEntities(ctx context.Context, args DeleteEntitiesArgs) (*mcp.ToolResponse, error) { -// ctx, span := tracer.Start(ctx, "DeleteEntities", tracerAttrs...) -// defer span.End() -// -// for _, entityName := range args.EntityNames { -// // Process each entity -// entity, err := l.DB.ReadEntityByName(ctx, entityName) -// if err != nil { -// zap.L().Error("Can't read entity from database", zap.Error(err), zap.String("entityName", entityName)) -// span.RecordError(err) -// return nil, err -// } -// if entity == nil { -// zap.L().Warn("Entity not found in database", zap.String("entityName", entityName)) -// continue -// } -// -// if err := l.DB.DeleteAllObservationsByEntityID(ctx, entity.ID); err != nil { -// zap.L().Error("Can't delete observations", zap.Error(err), zap.String("entityName", entityName)) -// span.RecordError(err) -// return nil, err -// } -// -// if err := l.DB.DeleteEntity(ctx, entity); err != nil { -// zap.L().Error("Can't delete entity", zap.Error(err), zap.String("entityName", entityName)) -// span.RecordError(err) -// return nil, err -// } -// } -// -// return mcp.NewToolResponse( -// mcp.NewTextContent("Entities deleted successfully"), -// ), nil -//} diff --git a/internal/logic/v1/graph.go b/internal/logic/v1/graph.go deleted file mode 100644 index 81f2c11..0000000 --- a/internal/logic/v1/graph.go +++ /dev/null @@ -1,77 +0,0 @@ -package v1 - -//// KnowledgeGraph represents the entire knowledge graph. -//type KnowledgeGraph struct { -// Entities []Entity `json:"entities"` -// Relations []Relation `json:"relations"` -//} -// -//// ReadGraphArgs represents the arguments for reading the knowledge graph. -//type ReadGraphArgs struct { -//} -// -//// ReadGraph reads the entire knowledge graph. -//func (l *Logic) ReadGraph(ctx context.Context, _ ReadGraphArgs) (*mcp.ToolResponse, error) { -// ctx, span := tracer.Start(ctx, "ReadGraph", tracerAttrs...) -// defer span.End() -// -// // Read entities -// zap.L().Debug("Reading all entities from the database") -// entities, err := l.DB.ReadAllEntities(ctx) -// if err != nil && !errors.Is(err, db.ErrNoEntries) { -// zap.L().Error("Can't read entities from the database", zap.Error(err)) -// span.RecordError(err) -// return nil, err -// } -// -// // Convert entities to response format -// zap.L().Debug("Converting entities to response format", zap.Any("entities", entities)) -// entitiesResponse := make([]Entity, 0) -// for _, entity := range entities { -// newEntity := Entity{ -// Name: entity.Name, -// Type: entity.Type, -// Observations: make([]string, 0), -// } -// for _, observation := range entity.Observations { -// newEntity.Observations = append(newEntity.Observations, observation.Contents) -// } -// entitiesResponse = append(entitiesResponse, newEntity) -// } -// -// // Read relations -// zap.L().Debug("Reading all relations from the database") -// relations, err := l.DB.ReadAllRelations(ctx) -// if err != nil && !errors.Is(err, db.ErrNoEntries) { -// span.RecordError(err) -// return nil, err -// } -// -// // Convert relations to response format -// zap.L().Debug("Converting relations to response format", zap.Any("relations", relations)) -// relationsResponse := make([]Relation, 0) -// for _, relation := range relations { -// newRelation := Relation{ -// From: relation.From.Name, -// To: relation.To.Name, -// Type: relation.Type, -// } -// relationsResponse = append(relationsResponse, newRelation) -// } -// -// // Create the knowledge graph -// zap.L().Debug("Creating knowledge graph", zap.Int("entities", len(entitiesResponse)), zap.Int("relations", len(relationsResponse))) -// graph := KnowledgeGraph{ -// Entities: entitiesResponse, -// Relations: relationsResponse, -// } -// -// // Convert response to json string -// zap.L().Debug("Converting knowledge graph to JSON", zap.Any("knowledge_graph", graph)) -// jsonResponse, err := toolJSONResponse(ctx, graph) -// if err != nil { -// span.RecordError(err) -// return nil, err -// } -// return jsonResponse, nil -//} diff --git a/internal/logic/v1/logic.go b/internal/logic/v1/logic.go index 8f4a206..4d8dbf4 100644 --- a/internal/logic/v1/logic.go +++ b/internal/logic/v1/logic.go @@ -70,12 +70,12 @@ func (l *Logic) CreateObservation(ctx context.Context, observation *models.Obser return logic.ProcessError(l.db.CreateObservation(ctx, observation)) } -func (l *Logic) DeleteAllObservationsByEntityID(ctx context.Context, entityID int64) error { - ctx, span := tracer.Start(ctx, "DeleteAllObservationsByEntityID", tracerAttrs...) - defer span.End() - - return logic.ProcessError(l.db.DeleteAllObservationsByEntityID(ctx, entityID)) -} +//func (l *Logic) DeleteAllObservationsByEntityID(ctx context.Context, entityID int64) error { +// ctx, span := tracer.Start(ctx, "DeleteAllObservationsByEntityID", tracerAttrs...) +// defer span.End() +// +// return logic.ProcessError(l.db.DeleteAllObservationsByEntityID(ctx, entityID)) +//} func (l *Logic) DeleteObservation(ctx context.Context, observation *models.Observation) error { ctx, span := tracer.Start(ctx, "DeleteObservation", tracerAttrs...) @@ -102,12 +102,12 @@ func (l *Logic) CreateRelation(ctx context.Context, relation *models.Relation) e return logic.ProcessError(l.db.CreateRelation(ctx, relation)) } -func (l *Logic) DeleteAllRelationsByEntityID(ctx context.Context, entityID int64) error { - ctx, span := tracer.Start(ctx, "DeleteAllRelationsByEntityID", tracerAttrs...) - defer span.End() - - return logic.ProcessError(l.db.DeleteAllRelationsByEntityID(ctx, entityID)) -} +//func (l *Logic) DeleteAllRelationsByEntityID(ctx context.Context, entityID int64) error { +// ctx, span := tracer.Start(ctx, "DeleteAllRelationsByEntityID", tracerAttrs...) +// defer span.End() +// +// return logic.ProcessError(l.db.DeleteAllRelationsByEntityID(ctx, entityID)) +//} func (l *Logic) ReadAllRelations(ctx context.Context) ([]*models.Relation, error) { ctx, span := tracer.Start(ctx, "ReadAllRelations", tracerAttrs...) @@ -137,19 +137,3 @@ func (l *Logic) DeleteRelation(ctx context.Context, relation *models.Relation) e return logic.ProcessError(l.db.DeleteRelation(ctx, relation)) } - -//func toolJSONResponse(ctx context.Context, response any) (*mcp.ToolResponse, error) { -// _, span := tracer.Start(ctx, "toolJSONResponse", tracerAttrs...) -// defer span.End() -// -// // convert response to json string -// jsonResponse, err := json.MarshalIndent(response, "", " ") -// if err != nil { -// span.RecordError(err) -// return nil, err -// } -// -// return mcp.NewToolResponse( -// mcp.NewTextContent(string(jsonResponse)), -// ), nil -//} diff --git a/internal/logic/v1/logic_test.go b/internal/logic/v1/logic_test.go deleted file mode 100644 index 1a5a528..0000000 --- a/internal/logic/v1/logic_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package v1 - -//type unmarshalable struct{} -// -//func (u unmarshalable) MarshalJSON() ([]byte, error) { -// return nil, errors.New("cannot marshal") -//} -// -//func Test_toolJSONResponse(t *testing.T) { -// tests := []struct { -// name string -// input any -// wantErr bool -// wantJSON string -// }{ -// { -// name: "simple map", -// input: map[string]int{"foo": 1}, -// wantErr: false, -// wantJSON: `{ -// "foo": 1 -//}`, -// }, -// { -// name: "unmarshalable type", -// input: unmarshalable{}, -// wantErr: true, -// }, -// } -// -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// resp, err := toolJSONResponse(context.Background(), tt.input) -// if tt.wantErr { -// assert.Error(t, err) -// assert.Nil(t, resp) -// } else { -// assert.NoError(t, err) -// if assert.NotNil(t, resp) { -// if assert.Len(t, resp.Content, 1) { -// assert.Equal(t, resp.Content[0].TextContent.Text, tt.wantJSON) -// } -// } -// } -// }) -// } -//} diff --git a/internal/logic/v1/nodes.go b/internal/logic/v1/nodes.go deleted file mode 100644 index a8b550c..0000000 --- a/internal/logic/v1/nodes.go +++ /dev/null @@ -1,31 +0,0 @@ -package v1 - -//// OpenNodesArgs represents the arguments for opening nodes. -//type OpenNodesArgs struct { -// Names []string `json:"names" jsonschema:"required,description=An array of entity names to retrieve"` -//} -// -//// OpenNodes opens nodes in the knowledge graph. -//func (l *Logic) OpenNodes(ctx context.Context, _ SearchNodesArgs) (*mcp.ToolResponse, error) { -// _, span := tracer.Start(ctx, "OpenNodes", tracerAttrs...) -// defer span.End() -// -// return mcp.NewToolResponse( -// mcp.NewTextContent("Open Nodes not implemented yet"), -// ), nil -//} -// -//// SearchNodesArgs represents the arguments for searching nodes. -//type SearchNodesArgs struct { -// Query string `json:"query" jsonschema:"required,description=The search query to match against entity names, types, and observation content"` -//} -// -//// SearchNodes searches for nodes in the knowledge graph. -//func (l *Logic) SearchNodes(ctx context.Context, _ SearchNodesArgs) (*mcp.ToolResponse, error) { -// _, span := tracer.Start(ctx, "SearchNodes", tracerAttrs...) -// defer span.End() -// -// return mcp.NewToolResponse( -// mcp.NewTextContent("Search Nodes not implemented yet"), -// ), nil -//} diff --git a/internal/logic/v1/observation.go b/internal/logic/v1/observation.go deleted file mode 100644 index 8826314..0000000 --- a/internal/logic/v1/observation.go +++ /dev/null @@ -1,122 +0,0 @@ -package v1 - -//// AddObservationsArgs represents the arguments for creating Observations. -//type AddObservationsArgs struct { -// Observations []AddObservation `json:"observations" jsonschema:"required,description=An array of observation contents to add"` -//} -// -//// AddObservation represents an observation associated with an entity. -//type AddObservation struct { -// EntityName string `json:"entityName" jsonschema:"required,description=The name of the entity to add the observations to"` -// Contents []string `json:"contents" jsonschema:"required,description=An array of observation contents to addAn array of observations"` -//} -// -//// AddedObservationsResp represents the response for creating Observations. -//type AddedObservationsResp struct { -// EntityName string `json:"entityName" jsonschema:"required,description=The name of the entity containing the observations"` -// AddedObservations []string `json:"addedObservations" jsonschema:"required,description=An array of observations"` -//} -// -//// AddObservations creates Observations on entities in the knowledge graph. -//func (l *Logic) AddObservations(ctx context.Context, args AddObservationsArgs) (*mcp.ToolResponse, error) { -// ctx, span := tracer.Start(ctx, "AddObservations", tracerAttrs...) -// defer span.End() -// -// response := make([]AddedObservationsResp, 0, len(args.Observations)) -// for _, observation := range args.Observations { -// entity, err := l.DB.ReadEntityByName(ctx, observation.EntityName) -// switch { -// case err != nil && !errors.Is(err, db.ErrNoEntries): -// zap.L().Error("Failed to read entity by name", zap.Error(err), zap.String("entity_name", observation.EntityName)) -// span.RecordError(err) -// return nil, err -// case errors.Is(err, db.ErrNoEntries): -// return mcp.NewToolResponse(mcp.NewTextContent(fmt.Sprintf("The entity %s was not found", observation.EntityName))), nil -// } -// newResponse := AddedObservationsResp{ -// EntityName: observation.EntityName, -// } -// -// for _, content := range observation.Contents { -// newObservation := &models.Observation{ -// EntityID: entity.ID, -// Contents: content, -// } -// -// if err := l.DB.CreateObservation(ctx, newObservation); err != nil { -// zap.L().Error("Failed to create observation", zap.Error(err), zap.String("entity_name", observation.EntityName), zap.String("content", content)) -// span.RecordError(err) -// return nil, err -// } -// newResponse.AddedObservations = append(newResponse.AddedObservations, newObservation.Contents) -// } -// -// response = append(response, newResponse) -// } -// -// // convert response to json string -// toolResponse, err := toolJSONResponse(ctx, response) -// if err != nil { -// zap.L().Error("json marshal error", zap.Error(err)) -// span.RecordError(err) -// return nil, err -// } -// -// return toolResponse, nil -//} -// -//// DeleteObservationsArgs represents the arguments for deleting Observations. -//type DeleteObservationsArgs struct { -// Deletions []DeleteObservation `json:"deletions" jsonschema:"required,description=An array of observations to delete"` -//} -// -//// DeleteObservation represents an observation associated with an entity. -//type DeleteObservation struct { -// EntityName string `json:"entityName" jsonschema:"required,description=The name of the entity containing the observations"` -// Observations []string `json:"observations" jsonschema:"required,description=An array of observations to delete"` -//} -// -//// DeleteObservations deletes Observations on entities in the knowledge graph. -//func (l *Logic) DeleteObservations(ctx context.Context, args DeleteObservationsArgs) (*mcp.ToolResponse, error) { -// ctx, span := tracer.Start(ctx, "DeleteObservations", tracerAttrs...) -// defer span.End() -// -// for _, observation := range args.Deletions { -// entity, err := l.DB.ReadEntityByName(ctx, observation.EntityName) -// switch { -// case err != nil && !errors.Is(err, db.ErrNoEntries): -// zap.L().Error("Failed to read entity by name", zap.Error(err), zap.String("entity_name", observation.EntityName)) -// span.RecordError(err) -// return nil, err -// case errors.Is(err, db.ErrNoEntries): -// return mcp.NewToolResponse(mcp.NewTextContent(fmt.Sprintf("The entity %s was not found", observation.EntityName))), nil -// } -// -// for _, content := range observation.Observations { -// // Read the observation by text -// observationToDelete, err := l.DB.ReadObservationByTextForEntityID(ctx, entity.ID, content) -// if err != nil { -// if errors.Is(err, db.ErrNoEntries) { -// // Observation not found, continue to the next one -// zap.L().Debug("Observation not found, skipping deletion", zap.String("entity_name", observation.EntityName), zap.String("content", content)) -// continue -// } -// zap.L().Error("Failed to read observation by text", zap.Error(err), zap.String("entity_name", observation.EntityName), zap.String("content", content)) -// span.RecordError(err) -// return nil, err -// } -// -// // Delete the observation -// zap.L().Debug("Deleting observation", zap.Int64("id", observationToDelete.ID), zap.String("content", content)) -// if err := l.DB.DeleteObservation(ctx, observationToDelete); err != nil { -// zap.L().Error("Failed to create observation", zap.Error(err), zap.Int64("id", observationToDelete.ID), zap.String("content", content)) -// span.RecordError(err) -// return nil, err -// } -// } -// } -// -// return mcp.NewToolResponse( -// mcp.NewTextContent("Observations deleted successfully"), -// ), nil -//} diff --git a/internal/logic/v1/relation.go b/internal/logic/v1/relation.go deleted file mode 100644 index 992a483..0000000 --- a/internal/logic/v1/relation.go +++ /dev/null @@ -1,116 +0,0 @@ -package v1 - -//// Relation represents a relationship between two entities in the knowledge graph. -//type Relation struct { -// From string `json:"from" jsonschema:"required,description=The name of the entity where the relation starts"` -// To string `json:"to" jsonschema:"required,description=The name of the entity where the relation ends"` -// Type string `json:"relationType" jsonschema:"required,description=The type of the relation"` -//} -// -//// CreateRelationsArgs represents the arguments for creating Relationships. -//type CreateRelationsArgs struct { -// Relations []Relation `json:"relations" jsonschema:"required,description=Create multiple new relations between entities in the knowledge graph. Relations should be in active voice"` -//} -// -//// CreateRelations creates relationships in the knowledge graph. -//func (l *Logic) CreateRelations(ctx context.Context, args CreateRelationsArgs) (*mcp.ToolResponse, error) { -// ctx, span := tracer.Start(ctx, "CreateRelations", tracerAttrs...) -// defer span.End() -// -// response := make([]Relation, 0, len(args.Relations)) -// for _, relation := range args.Relations { -// entityFrom, err := l.DB.ReadEntityByName(ctx, relation.From) -// if err != nil { -// return nil, err -// } -// zap.L().Debug("got from entity", zap.Any("entity", entityFrom)) -// -// entityTo, err := l.DB.ReadEntityByName(ctx, relation.To) -// if err != nil { -// return nil, err -// } -// zap.L().Debug("got to entity", zap.Any("entity", entityTo)) -// -// newRelation := &models.Relation{ -// FromID: entityFrom.ID, -// ToID: entityTo.ID, -// Type: relation.Type, -// } -// if err := l.DB.CreateRelation(ctx, newRelation); err != nil { -// return nil, err -// } -// -// response = append(response, Relation{ -// From: entityFrom.Name, -// To: entityTo.Name, -// Type: newRelation.Type, -// }) -// } -// -// // convert response to json string -// toolResponse, err := toolJSONResponse(ctx, response) -// if err != nil { -// zap.L().Error("json marshal error", zap.Error(err)) -// span.RecordError(err) -// return nil, err -// } -// -// return toolResponse, nil -//} -// -//// DeleteRelationsArgs represents the arguments for deleting Relationships. -//type DeleteRelationsArgs struct { -// Relations []Relation `json:"relations" jsonschema:"required,description=Delete multiple relations from the knowledge graph"` -//} -// -//// DeleteRelations deletes relationships from the knowledge graph. -//func (l *Logic) DeleteRelations(ctx context.Context, args DeleteRelationsArgs) (*mcp.ToolResponse, error) { -// ctx, span := tracer.Start(ctx, "DeleteRelations", tracerAttrs...) -// defer span.End() -// -// for _, relation := range args.Relations { -// entityFrom, err := l.DB.ReadEntityByName(ctx, relation.From) -// switch { -// case errors.Is(err, db.ErrNoEntries): -// zap.L().Debug("entity not found", zap.String("entity", relation.From), zap.String("position", "from")) -// return mcp.NewToolResponse( -// mcp.NewTextContent(fmt.Sprintf("Entity %s was not found", relation.From)), -// ), nil -// case err != nil: -// zap.L().Error("read from entity error", zap.Error(err)) -// span.RecordError(err) -// return nil, err -// default: -// zap.L().Debug("got from entity", zap.Any("entity", entityFrom)) -// } -// -// entityTo, err := l.DB.ReadEntityByName(ctx, relation.To) -// switch { -// case errors.Is(err, db.ErrNoEntries): -// zap.L().Debug("entity not found", zap.String("entity", relation.To), zap.String("position", "to")) -// return mcp.NewToolResponse( -// mcp.NewTextContent(fmt.Sprintf("Entity %s was not found", relation.To)), -// ), nil -// case err != nil: -// zap.L().Error("read to entity error", zap.Error(err)) -// span.RecordError(err) -// return nil, err -// default: -// zap.L().Debug("got to entity", zap.Any("entity", entityFrom)) -// } -// -// // find the relation -// existingRelation, err := l.DB.ReadExactRelation(ctx, entityFrom.ID, entityTo.ID, relation.Type) -// if err != nil { -// return nil, err -// } -// -// if err := l.DB.DeleteRelation(ctx, existingRelation); err != nil { -// return nil, err -// } -// } -// -// return mcp.NewToolResponse( -// mcp.NewTextContent("Relations deleted successfully"), -// ), nil -//} diff --git a/internal/models/models.go b/internal/models/models.go index 729f109..ae37c51 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -1,3 +1,3 @@ package models -// Package models contains the data models for the application. +// Package apiv1 contains the data apiv1 for the application. diff --git a/internal/util/tool_json_response_test.go b/internal/util/tool_json_response_test.go index f26c955..0eef428 100644 --- a/internal/util/tool_json_response_test.go +++ b/internal/util/tool_json_response_test.go @@ -1,7 +1,6 @@ package util import ( - "context" "errors" "testing" @@ -15,6 +14,8 @@ func (u unmarshalable) MarshalJSON() ([]byte, error) { } func Test_toolJSONResponse(t *testing.T) { + t.Parallel() + tests := []struct { name string input any @@ -36,17 +37,18 @@ func Test_toolJSONResponse(t *testing.T) { }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - resp, err := ToolJSONResponse(context.Background(), tt.input) - if tt.wantErr { + for _, row := range tests { + t.Run(row.name, func(t *testing.T) { + t.Parallel() + resp, err := ToolJSONResponse(t.Context(), row.input) + if row.wantErr { assert.Error(t, err) assert.Nil(t, resp) } else { assert.NoError(t, err) if assert.NotNil(t, resp) { if assert.Len(t, resp.Content, 1) { - assert.Equal(t, resp.Content[0].TextContent.Text, tt.wantJSON) + assert.Equal(t, resp.Content[0].TextContent.Text, row.wantJSON) } } } diff --git a/pkg/api/entity.go b/pkg/api/entity.go new file mode 100644 index 0000000..b0e1916 --- /dev/null +++ b/pkg/api/entity.go @@ -0,0 +1,24 @@ +package api + +// Entity represents an entity in the knowledge graph. +type Entity struct { + Name string `json:"name" validate:"required"` + Type string `json:"entityType" validate:"required"` + Observations []string `json:"observations"` +} + +type CreateEntitiesRequest struct { + Entities []Entity `json:"entities" validate:"required,min=1"` +} + +type CreateEntitiesResponse struct { + Entities []Entity `json:"entities"` +} + +type DeleteEntitiesRequest struct { + EntityNames []string `json:"entityNames" validate:"required,min=1"` +} + +type DeleteEntitiesResponse struct { + Success bool `json:"success"` +} diff --git a/pkg/api/entity_test.go b/pkg/api/entity_test.go new file mode 100644 index 0000000..9456aaf --- /dev/null +++ b/pkg/api/entity_test.go @@ -0,0 +1,80 @@ +package api + +import ( + "errors" + "testing" + + "github.com/go-playground/validator/v10" + "github.com/stretchr/testify/assert" +) + +func testEntityValidation(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input *Entity + expectErr bool + errors []struct { + Namespace string + Tag string + } + }{ + { + name: "empty entity", + input: &Entity{ + Name: "", + Type: "", + Observations: nil, + }, + expectErr: true, + errors: []struct { + Namespace string + Tag string + }{ + { + Namespace: "Entity.Type", + Tag: "required", + }, + { + Namespace: "Entity.Name", + Tag: "required", + }, + }, + }, + } + + validate := validator.New(validator.WithRequiredStructEnabled()) + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + err := validate.Struct(tt.input) + if tt.expectErr { + assert.Error(t, err) + + var validateErrs validator.ValidationErrors + if errors.As(err, &validateErrs) { + assert.Equal(t, 0, len(validateErrs)) + for _, e := range validateErrs { + assert.Equal(t, tt.errors[i].Namespace, e.Namespace()) + assert.Equal(t, tt.errors[i].Tag, e.Tag()) + t.Log(e.Namespace()) + t.Log(e.Field()) + t.Log(e.StructNamespace()) + t.Log(e.StructField()) + t.Log(e.Tag()) + t.Log(e.ActualTag()) + t.Log(e.Kind()) + t.Log(e.Type()) + t.Log(e.Value()) + t.Log(e.Param()) + t.Log() + } + } else { + assert.Fail(t, "Expected validation errors, but got: %T", err) + } + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/api/graph.go b/pkg/api/graph.go new file mode 100644 index 0000000..eccb92a --- /dev/null +++ b/pkg/api/graph.go @@ -0,0 +1,14 @@ +package api + +// KnowledgeGraph represents the entire knowledge graph. +type KnowledgeGraph struct { + Entities []Entity `json:"entities"` + Relations []Relation `json:"relations"` +} + +type ReadGraphRequest struct { +} + +type ReadGraphResponse struct { + KnowledgeGraph KnowledgeGraph `json:"knowledgeGraph"` +} diff --git a/pkg/api/graph_test.go b/pkg/api/graph_test.go new file mode 100644 index 0000000..778f64e --- /dev/null +++ b/pkg/api/graph_test.go @@ -0,0 +1 @@ +package api diff --git a/pkg/api/node.go b/pkg/api/node.go new file mode 100644 index 0000000..b05b388 --- /dev/null +++ b/pkg/api/node.go @@ -0,0 +1,13 @@ +package api + +type OpenNodesRequest struct { +} + +type OpenNodesResponse struct { +} + +type SearchNodesRequest struct { +} + +type SearchNodesResponse struct { +} diff --git a/pkg/api/observation.go b/pkg/api/observation.go new file mode 100644 index 0000000..974270f --- /dev/null +++ b/pkg/api/observation.go @@ -0,0 +1,20 @@ +package api + +// AddObservation represents an observation associated with an entity. +type AddObservation struct { + EntityName string `json:"entityName" validate:"required"` + Contents []string `json:"contents" validate:"required,min=1"` +} +type AddObservationsRequest struct { + Observations []AddObservation `json:"observations" validate:"required,min=1"` +} + +type AddObservationsResponse struct { +} + +type DeleteObservationsRequest struct { +} + +type DeleteObservationsResponse struct { + Success bool `json:"success"` +} diff --git a/pkg/api/relation.go b/pkg/api/relation.go new file mode 100644 index 0000000..f46463a --- /dev/null +++ b/pkg/api/relation.go @@ -0,0 +1,21 @@ +package api + +// Relation represents a relationship between two entities in the knowledge graph. +type Relation struct { + From string `json:"from" validate:"required"` + To string `json:"to" validate:"required"` + Type string `json:"relationType" validate:"required"` +} + +type CreateRelationsRequest struct { +} + +type CreateRelationsResponse struct { +} + +type DeleteRelationsRequest struct { +} + +type DeleteRelationsResponse struct { + Success bool `json:"success"` +} diff --git a/pkg/client/client.go b/pkg/client/client.go new file mode 100644 index 0000000..da13c8e --- /dev/null +++ b/pkg/client/client.go @@ -0,0 +1 @@ +package client diff --git a/scripts/build.ps1 b/scripts/build.ps1 new file mode 100644 index 0000000..6cb9a2a --- /dev/null +++ b/scripts/build.ps1 @@ -0,0 +1 @@ +goreleaser build --clean --snapshot