From 810cb20ea0f70a0c965f55425364ce9cfbba2c95 Mon Sep 17 00:00:00 2001 From: Tyr Mactire Date: Wed, 14 May 2025 17:53:13 -0700 Subject: [PATCH 01/15] cleanup --- internal/adapter/direct.go | 1 - internal/logic/v1/entity.go | 105 ------------------- internal/logic/v1/graph.go | 77 -------------- internal/logic/v1/logic.go | 16 --- internal/logic/v1/logic_test.go | 47 --------- internal/logic/v1/nodes.go | 31 ------ internal/logic/v1/observation.go | 122 ----------------------- internal/logic/v1/relation.go | 116 --------------------- internal/models/models.go | 2 +- internal/util/tool_json_response_test.go | 2 +- 10 files changed, 2 insertions(+), 517 deletions(-) delete mode 100644 internal/logic/v1/entity.go delete mode 100644 internal/logic/v1/graph.go delete mode 100644 internal/logic/v1/logic_test.go delete mode 100644 internal/logic/v1/nodes.go delete mode 100644 internal/logic/v1/observation.go delete mode 100644 internal/logic/v1/relation.go diff --git a/internal/adapter/direct.go b/internal/adapter/direct.go index 869799a..2d63f51 100644 --- a/internal/adapter/direct.go +++ b/internal/adapter/direct.go @@ -334,7 +334,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) { 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..4b7332e 100644 --- a/internal/logic/v1/logic.go +++ b/internal/logic/v1/logic.go @@ -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..aef2ccc 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 api contains the data api for the application. diff --git a/internal/util/tool_json_response_test.go b/internal/util/tool_json_response_test.go index f26c955..92b39a6 100644 --- a/internal/util/tool_json_response_test.go +++ b/internal/util/tool_json_response_test.go @@ -38,7 +38,7 @@ 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) + resp, err := ToolJSONResponse(t.Context(), tt.input) if tt.wantErr { assert.Error(t, err) assert.Nil(t, resp) From a4f3384f938d9508754eb167dd125823990b6680 Mon Sep 17 00:00:00 2001 From: Tyr Mactire Date: Wed, 14 May 2025 17:56:15 -0700 Subject: [PATCH 02/15] lint --- .golangci.yml | 1 + internal/adapter/adapter.go | 20 ++++++++++---------- internal/adapter/direct.go | 8 ++++---- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 94aad9f..f5a3795 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -94,6 +94,7 @@ linters: allow: - error - db.Error + - logic.Error mnd: ignored-files: diff --git a/internal/adapter/adapter.go b/internal/adapter/adapter.go index 4541288..8a39a63 100644 --- a/internal/adapter/adapter.go +++ b/internal/adapter/adapter.go @@ -19,32 +19,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/direct.go b/internal/adapter/direct.go index 2d63f51..f22c1ef 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() From 77c9f051dda1f78db4c98cf04818501044b19037 Mon Sep 17 00:00:00 2001 From: Tyr Mactire Date: Wed, 14 May 2025 18:00:25 -0700 Subject: [PATCH 03/15] cleanup --- internal/util/tool_json_response_test.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/internal/util/tool_json_response_test.go b/internal/util/tool_json_response_test.go index 92b39a6..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(t.Context(), 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) } } } From d58a9729e148a341884107ec15c4f924aa736092 Mon Sep 17 00:00:00 2001 From: Tyr Mactire Date: Wed, 14 May 2025 18:04:31 -0700 Subject: [PATCH 04/15] api models --- pkg/api/entity.go | 13 +++++++++++++ pkg/api/graph.go | 7 +++++++ pkg/api/node.go | 13 +++++++++++++ pkg/api/observation.go | 13 +++++++++++++ pkg/api/relation.go | 13 +++++++++++++ scripts/build.ps1 | 1 + 6 files changed, 60 insertions(+) create mode 100644 pkg/api/entity.go create mode 100644 pkg/api/graph.go create mode 100644 pkg/api/node.go create mode 100644 pkg/api/observation.go create mode 100644 pkg/api/relation.go create mode 100644 scripts/build.ps1 diff --git a/pkg/api/entity.go b/pkg/api/entity.go new file mode 100644 index 0000000..be51289 --- /dev/null +++ b/pkg/api/entity.go @@ -0,0 +1,13 @@ +package api + +type CreateEntitiesRequest struct { +} + +type CreateEntitiesResponse struct { +} + +type DeleteEntitiesRequest struct { +} + +type DeleteEntitiesResponse struct { +} diff --git a/pkg/api/graph.go b/pkg/api/graph.go new file mode 100644 index 0000000..c6a2918 --- /dev/null +++ b/pkg/api/graph.go @@ -0,0 +1,7 @@ +package api + +type ReadGraphRequest struct { +} + +type ReadGraphResponse struct { +} 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..23d936a --- /dev/null +++ b/pkg/api/observation.go @@ -0,0 +1,13 @@ +package api + +type AddObservationsRequest struct { +} + +type AddObservationsResponse struct { +} + +type DeleteObservationsRequest struct { +} + +type DeleteObservationsResponse struct { +} diff --git a/pkg/api/relation.go b/pkg/api/relation.go new file mode 100644 index 0000000..14ea1de --- /dev/null +++ b/pkg/api/relation.go @@ -0,0 +1,13 @@ +package api + +type CreateRelationsRequest struct { +} + +type CreateRelationsResponse struct { +} + +type DeleteRelationsRequest struct { +} + +type DeleteRelationsResponse struct { +} 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 From 6d64eaf8fae20e6137abe5a3eb48156ec214bf05 Mon Sep 17 00:00:00 2001 From: Tyr Mactire Date: Mon, 19 May 2025 16:35:09 -0700 Subject: [PATCH 05/15] progress --- cmd/mcp_dbmem/action/server/server.go | 104 ++++++++++++++++++++++++++ cmd/mcp_dbmem/flag/server.go | 11 +++ cmd/mcp_dbmem/main.go | 66 +++++++++++----- internal/adapter/common.go | 1 - internal/adapter/server.go | 97 ++++++++++++++++++++++++ pkg/client/client.go | 1 + 6 files changed, 261 insertions(+), 19 deletions(-) create mode 100644 cmd/mcp_dbmem/action/server/server.go create mode 100644 cmd/mcp_dbmem/flag/server.go delete mode 100644 internal/adapter/common.go create mode 100644 internal/adapter/server.go create mode 100644 pkg/client/client.go diff --git a/cmd/mcp_dbmem/action/server/server.go b/cmd/mcp_dbmem/action/server/server.go new file mode 100644 index 0000000..3cd06e2 --- /dev/null +++ b/cmd/mcp_dbmem/action/server/server.go @@ -0,0 +1,104 @@ +package server + +import ( + "context" + "fmt" + "os" + "os/signal" + "syscall" + + mcp "github.com/metoro-io/mcp-golang" + "github.com/metoro-io/mcp-golang/transport/stdio" + "github.com/spf13/viper" + "github.com/tyrm/mcp-dbmem/cmd/mcp_dbmem/action" + "github.com/tyrm/mcp-dbmem/internal/adapter" + "github.com/tyrm/mcp-dbmem/internal/config" + "github.com/tyrm/mcp-dbmem/internal/db/bun" + 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("starting pgmcp server") + + // Setup tracing + if viper.GetString(config.Keys.UptraceDSN) != "" { + uptrace.ConfigureOpentelemetry( + uptrace.WithServiceName("mcp-dbmem"), + 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)) + + 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, + }) + + direct := adapter.NewDirectAdapter(logic) + + // add tools + server := mcp.NewServer(stdio.NewStdioServerTransport()) + if err := direct.Apply(server); err != nil { + zap.L().Error("Error applying direct adapter", zap.Error(err)) + + return err + } + + // ** 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 mcp server + go func(s *mcp.Server, errChan chan error) { + zap.L().Info("starting mcp server") + err := s.Serve() + if err != nil { + errChan <- fmt.Errorf("mcp server: %s", err.Error()) + } + }(server, 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)) + } + + 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..b6bf760 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" @@ -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/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/server.go b/internal/adapter/server.go new file mode 100644 index 0000000..c447921 --- /dev/null +++ b/internal/adapter/server.go @@ -0,0 +1,97 @@ +package adapter + +import ( + "context" + + mcp "github.com/metoro-io/mcp-golang" + "github.com/tyrm/mcp-dbmem/internal/logic" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +var serverTracer = otel.Tracer("internal/adapter.ServerAdapter") +var serverTracerAttrs = []trace.SpanStartOption{ + trace.WithAttributes( + attribute.String("service.name", "mcp-dbmem"), + attribute.String("component", "server"), + attribute.String("span.kind", "server"), + ), +} + +type ServerAdapter struct { + logic logic.Logic +} + +func NewServerAdapter(logic logic.Logic) *ServerAdapter { + return &ServerAdapter{ + logic: logic, + } +} + +func (d *ServerAdapter) Apply(server *mcp.Server) error { + return apply(d, server) +} + +func (d *ServerAdapter) CreateEntities(ctx context.Context, args CreateEntitiesArgs) (*mcp.ToolResponse, error) { + ctx, span := serverTracer.Start(ctx, "CreateEntities", serverTracerAttrs...) + defer span.End() + + return nil, nil +} + +func (d *ServerAdapter) DeleteEntities(ctx context.Context, args DeleteEntitiesArgs) (*mcp.ToolResponse, error) { + ctx, span := serverTracer.Start(ctx, "DeleteEntities", serverTracerAttrs...) + defer span.End() + + return nil, nil +} + +func (d *ServerAdapter) ReadGraph(ctx context.Context, args ReadGraphArgs) (*mcp.ToolResponse, error) { + ctx, span := serverTracer.Start(ctx, "ReadGraph", serverTracerAttrs...) + defer span.End() + + return nil, nil +} + +func (d *ServerAdapter) OpenNodes(ctx context.Context, args OpenNodesArgs) (*mcp.ToolResponse, error) { + ctx, span := serverTracer.Start(ctx, "OpenNodes", serverTracerAttrs...) + defer span.End() + + return nil, nil +} + +func (d *ServerAdapter) SearchNodes(ctx context.Context, args SearchNodesArgs) (*mcp.ToolResponse, error) { + ctx, span := serverTracer.Start(ctx, "SearchNodes", serverTracerAttrs...) + defer span.End() + + return nil, nil +} + +func (d *ServerAdapter) AddObservations(ctx context.Context, args AddObservationsArgs) (*mcp.ToolResponse, error) { + ctx, span := serverTracer.Start(ctx, "AddObservations", serverTracerAttrs...) + defer span.End() + + return nil, nil +} + +func (d *ServerAdapter) DeleteObservations(ctx context.Context, args DeleteObservationsArgs) (*mcp.ToolResponse, error) { + ctx, span := serverTracer.Start(ctx, "DeleteObservations", serverTracerAttrs...) + defer span.End() + + return nil, nil +} + +func (d *ServerAdapter) CreateRelations(ctx context.Context, args CreateRelationsArgs) (*mcp.ToolResponse, error) { + ctx, span := serverTracer.Start(ctx, "CreateRelations", serverTracerAttrs...) + defer span.End() + + return nil, nil +} + +func (d *ServerAdapter) DeleteRelations(ctx context.Context, args DeleteRelationsArgs) (*mcp.ToolResponse, error) { + ctx, span := serverTracer.Start(ctx, "DeleteRelations", serverTracerAttrs...) + defer span.End() + + return nil, nil +} 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 From d5af08d39c1c39c57153f30092ad52e0a2d08794 Mon Sep 17 00:00:00 2001 From: Tyr Mactire Date: Mon, 19 May 2025 16:59:15 -0700 Subject: [PATCH 06/15] updates --- cmd/mcp_dbmem/action/direct/direct.go | 32 +++++---------------------- cmd/mcp_dbmem/action/server/server.go | 4 ++-- cmd/mcp_dbmem/main.go | 2 +- internal/adapter/server.go | 9 ++------ 4 files changed, 10 insertions(+), 37 deletions(-) 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 index 3cd06e2..c8499a9 100644 --- a/cmd/mcp_dbmem/action/server/server.go +++ b/cmd/mcp_dbmem/action/server/server.go @@ -21,12 +21,12 @@ import ( // 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("starting pgmcp server") + zap.L().Info(fmt.Sprintf("starting %s server", 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)), ) diff --git a/cmd/mcp_dbmem/main.go b/cmd/mcp_dbmem/main.go index b6bf760..71f0595 100644 --- a/cmd/mcp_dbmem/main.go +++ b/cmd/mcp_dbmem/main.go @@ -47,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, diff --git a/internal/adapter/server.go b/internal/adapter/server.go index c447921..2d836e9 100644 --- a/internal/adapter/server.go +++ b/internal/adapter/server.go @@ -4,6 +4,7 @@ import ( "context" mcp "github.com/metoro-io/mcp-golang" + "github.com/tyrm/mcp-dbmem/internal/config" "github.com/tyrm/mcp-dbmem/internal/logic" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" @@ -11,13 +12,7 @@ import ( ) var serverTracer = otel.Tracer("internal/adapter.ServerAdapter") -var serverTracerAttrs = []trace.SpanStartOption{ - trace.WithAttributes( - attribute.String("service.name", "mcp-dbmem"), - attribute.String("component", "server"), - attribute.String("span.kind", "server"), - ), -} +var serverTracerAttrs = []trace.SpanStartOption type ServerAdapter struct { logic logic.Logic From 7d2551d00f9fb66bd914b48895b2d36ba0cdeb8c Mon Sep 17 00:00:00 2001 From: Tyr Mactire Date: Mon, 19 May 2025 17:24:00 -0700 Subject: [PATCH 07/15] ws --- cmd/mcp_dbmem/action/server/server.go | 2 +- go.mod | 1 + go.sum | 2 + internal/adapter/server.go | 93 +++++++++++++++++++++++++-- 4 files changed, 92 insertions(+), 6 deletions(-) diff --git a/cmd/mcp_dbmem/action/server/server.go b/cmd/mcp_dbmem/action/server/server.go index c8499a9..2d905b8 100644 --- a/cmd/mcp_dbmem/action/server/server.go +++ b/cmd/mcp_dbmem/action/server/server.go @@ -65,7 +65,7 @@ var Server action.Action = func(ctx context.Context, _ []string) error { DB: dbClient, }) - direct := adapter.NewDirectAdapter(logic) + direct := adapter.NewServerAdapter(logic) // add tools server := mcp.NewServer(stdio.NewStdioServerTransport()) diff --git a/go.mod b/go.mod index b8fbe5d..6997c00 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // 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 diff --git a/go.sum b/go.sum index 278e6d5..fa25efb 100644 --- a/go.sum +++ b/go.sum @@ -47,6 +47,8 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC 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/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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= diff --git a/internal/adapter/server.go b/internal/adapter/server.go index 2d836e9..23f66c1 100644 --- a/internal/adapter/server.go +++ b/internal/adapter/server.go @@ -2,28 +2,34 @@ package adapter import ( "context" + "encoding/json" + "log" + "net/http" + "github.com/gorilla/websocket" mcp "github.com/metoro-io/mcp-golang" - "github.com/tyrm/mcp-dbmem/internal/config" "github.com/tyrm/mcp-dbmem/internal/logic" "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" ) var serverTracer = otel.Tracer("internal/adapter.ServerAdapter") -var serverTracerAttrs = []trace.SpanStartOption +var serverTracerAttrs []trace.SpanStartOption type ServerAdapter struct { - logic logic.Logic + handlers map[string]RPCHandler + logic logic.Logic } func NewServerAdapter(logic logic.Logic) *ServerAdapter { return &ServerAdapter{ - logic: logic, + handlers: make(map[string]RPCHandler), + logic: logic, } } +// api + func (d *ServerAdapter) Apply(server *mcp.Server) error { return apply(d, server) } @@ -90,3 +96,80 @@ func (d *ServerAdapter) DeleteRelations(ctx context.Context, args DeleteRelation return nil, nil } + +// websockets + +type RPCHandler func(params json.RawMessage) (any, error) + +type RPCMessage struct { + ID string `json:"id"` + Method string `json:"method"` + Params json.RawMessage `json:"params,omitempty"` + Result json.RawMessage `json:"result,omitempty"` + Error *RPCError `json:"error,omitempty"` +} + +type RPCError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { return true }, +} + +func (d *ServerAdapter) Register(method string, handler RPCHandler) { + d.handlers[method] = handler +} +func (d *ServerAdapter) handleWebSocket(w http.ResponseWriter, r *http.Request) { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Printf("Upgrade error: %v", err) + return + } + defer conn.Close() + + for { + var msg RPCMessage + err := conn.ReadJSON(&msg) + if err != nil { + log.Printf("Read error: %v", err) + break + } + + // Handle the RPC call + response := d.handleRPC(msg) + + err = conn.WriteJSON(response) + if err != nil { + log.Printf("Write error: %v", err) + break + } + } +} + +func (d *ServerAdapter) handleRPC(req RPCMessage) RPCMessage { + response := RPCMessage{ID: req.ID} + + handler, exists := d.handlers[req.Method] + if !exists { + response.Error = &RPCError{ + Code: -32601, + Message: "Method not found", + } + return response + } + + result, err := handler(req.Params) + if err != nil { + response.Error = &RPCError{ + Code: -32000, + Message: err.Error(), + } + } else { + resultBytes, _ := json.Marshal(result) + response.Result = resultBytes + } + + return response +} From c335744cca74c58f967a1cf0be6a24d4aae07d88 Mon Sep 17 00:00:00 2001 From: Tyr Mactire Date: Tue, 20 May 2025 12:37:21 -0700 Subject: [PATCH 08/15] chaos --- cmd/mcp_dbmem/action/server/server.go | 34 ++--- go.mod | 36 ++++-- go.sum | 64 +++++++++- internal/adapter/server.go | 175 -------------------------- internal/http/server.go | 100 +++++++++++++++ 5 files changed, 208 insertions(+), 201 deletions(-) delete mode 100644 internal/adapter/server.go create mode 100644 internal/http/server.go diff --git a/cmd/mcp_dbmem/action/server/server.go b/cmd/mcp_dbmem/action/server/server.go index 2d905b8..5461ea6 100644 --- a/cmd/mcp_dbmem/action/server/server.go +++ b/cmd/mcp_dbmem/action/server/server.go @@ -7,13 +7,11 @@ import ( "os/signal" "syscall" - mcp "github.com/metoro-io/mcp-golang" - "github.com/metoro-io/mcp-golang/transport/stdio" "github.com/spf13/viper" "github.com/tyrm/mcp-dbmem/cmd/mcp_dbmem/action" - "github.com/tyrm/mcp-dbmem/internal/adapter" "github.com/tyrm/mcp-dbmem/internal/config" "github.com/tyrm/mcp-dbmem/internal/db/bun" + "github.com/tyrm/mcp-dbmem/internal/http" v1 "github.com/tyrm/mcp-dbmem/internal/logic/v1" "github.com/uptrace/uptrace-go/uptrace" "go.uber.org/zap" @@ -22,6 +20,7 @@ import ( // 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) != "" { @@ -51,6 +50,7 @@ var Server action.Action = func(ctx context.Context, _ []string) error { }) if err != nil { zap.L().Error("Error creating bun client", zap.Error(err)) + cancel() return err } @@ -65,13 +65,14 @@ var Server action.Action = func(ctx context.Context, _ []string) error { DB: dbClient, }) - direct := adapter.NewServerAdapter(logic) - - // add tools - server := mcp.NewServer(stdio.NewStdioServerTransport()) - if err := direct.Apply(server); err != nil { - zap.L().Error("Error applying direct adapter", zap.Error(err)) - + httpServer, err := http.NewServer(ctx, http.ServerConfig{ + Logic: logic, + ApplicationName: config.ApplicationName, + HttpBind: "", + }) + if err != nil { + zap.L().Error("can't start http server", zap.Error(err)) + cancel() return err } @@ -82,14 +83,14 @@ var Server action.Action = func(ctx context.Context, _ []string) error { stopSigChan := make(chan os.Signal, 1) signal.Notify(stopSigChan, syscall.SIGINT, syscall.SIGTERM) - // start mcp server - go func(s *mcp.Server, errChan chan error) { - zap.L().Info("starting mcp server") - err := s.Serve() + // 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("mcp server: %s", err.Error()) + errChan <- fmt.Errorf("http server: %s", err.Error()) } - }(server, errChan) + }(httpServer, errChan) // wait for event select { @@ -99,6 +100,7 @@ var Server action.Action = func(ctx context.Context, _ []string) error { zap.L().Fatal("fatal error", zap.Error(err)) } + cancel() zap.L().Info("done") return nil } diff --git a/go.mod b/go.mod index 6997c00..45010b4 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,17 @@ 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/sirupsen/logrus package. - github.com/jackc/pgx => github.com/jackc/pgx v3.6.2+incompatible -// 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 + // 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/sirupsen/logrus package. + github.com/jackc/pgx => github.com/jackc/pgx v3.6.2+incompatible + // 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.0 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 +28,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,15 +40,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/websocket v1.5.3 // 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 @@ -56,8 +70,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 @@ -74,6 +93,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 @@ -92,6 +113,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 fa25efb..01c0d5b 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.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/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,24 +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/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +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= @@ -108,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= @@ -118,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= @@ -134,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= @@ -177,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= @@ -184,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= @@ -200,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= @@ -224,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= @@ -269,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= @@ -405,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/server.go b/internal/adapter/server.go deleted file mode 100644 index 23f66c1..0000000 --- a/internal/adapter/server.go +++ /dev/null @@ -1,175 +0,0 @@ -package adapter - -import ( - "context" - "encoding/json" - "log" - "net/http" - - "github.com/gorilla/websocket" - mcp "github.com/metoro-io/mcp-golang" - "github.com/tyrm/mcp-dbmem/internal/logic" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/trace" -) - -var serverTracer = otel.Tracer("internal/adapter.ServerAdapter") -var serverTracerAttrs []trace.SpanStartOption - -type ServerAdapter struct { - handlers map[string]RPCHandler - logic logic.Logic -} - -func NewServerAdapter(logic logic.Logic) *ServerAdapter { - return &ServerAdapter{ - handlers: make(map[string]RPCHandler), - logic: logic, - } -} - -// api - -func (d *ServerAdapter) Apply(server *mcp.Server) error { - return apply(d, server) -} - -func (d *ServerAdapter) CreateEntities(ctx context.Context, args CreateEntitiesArgs) (*mcp.ToolResponse, error) { - ctx, span := serverTracer.Start(ctx, "CreateEntities", serverTracerAttrs...) - defer span.End() - - return nil, nil -} - -func (d *ServerAdapter) DeleteEntities(ctx context.Context, args DeleteEntitiesArgs) (*mcp.ToolResponse, error) { - ctx, span := serverTracer.Start(ctx, "DeleteEntities", serverTracerAttrs...) - defer span.End() - - return nil, nil -} - -func (d *ServerAdapter) ReadGraph(ctx context.Context, args ReadGraphArgs) (*mcp.ToolResponse, error) { - ctx, span := serverTracer.Start(ctx, "ReadGraph", serverTracerAttrs...) - defer span.End() - - return nil, nil -} - -func (d *ServerAdapter) OpenNodes(ctx context.Context, args OpenNodesArgs) (*mcp.ToolResponse, error) { - ctx, span := serverTracer.Start(ctx, "OpenNodes", serverTracerAttrs...) - defer span.End() - - return nil, nil -} - -func (d *ServerAdapter) SearchNodes(ctx context.Context, args SearchNodesArgs) (*mcp.ToolResponse, error) { - ctx, span := serverTracer.Start(ctx, "SearchNodes", serverTracerAttrs...) - defer span.End() - - return nil, nil -} - -func (d *ServerAdapter) AddObservations(ctx context.Context, args AddObservationsArgs) (*mcp.ToolResponse, error) { - ctx, span := serverTracer.Start(ctx, "AddObservations", serverTracerAttrs...) - defer span.End() - - return nil, nil -} - -func (d *ServerAdapter) DeleteObservations(ctx context.Context, args DeleteObservationsArgs) (*mcp.ToolResponse, error) { - ctx, span := serverTracer.Start(ctx, "DeleteObservations", serverTracerAttrs...) - defer span.End() - - return nil, nil -} - -func (d *ServerAdapter) CreateRelations(ctx context.Context, args CreateRelationsArgs) (*mcp.ToolResponse, error) { - ctx, span := serverTracer.Start(ctx, "CreateRelations", serverTracerAttrs...) - defer span.End() - - return nil, nil -} - -func (d *ServerAdapter) DeleteRelations(ctx context.Context, args DeleteRelationsArgs) (*mcp.ToolResponse, error) { - ctx, span := serverTracer.Start(ctx, "DeleteRelations", serverTracerAttrs...) - defer span.End() - - return nil, nil -} - -// websockets - -type RPCHandler func(params json.RawMessage) (any, error) - -type RPCMessage struct { - ID string `json:"id"` - Method string `json:"method"` - Params json.RawMessage `json:"params,omitempty"` - Result json.RawMessage `json:"result,omitempty"` - Error *RPCError `json:"error,omitempty"` -} - -type RPCError struct { - Code int `json:"code"` - Message string `json:"message"` -} - -var upgrader = websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { return true }, -} - -func (d *ServerAdapter) Register(method string, handler RPCHandler) { - d.handlers[method] = handler -} -func (d *ServerAdapter) handleWebSocket(w http.ResponseWriter, r *http.Request) { - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - log.Printf("Upgrade error: %v", err) - return - } - defer conn.Close() - - for { - var msg RPCMessage - err := conn.ReadJSON(&msg) - if err != nil { - log.Printf("Read error: %v", err) - break - } - - // Handle the RPC call - response := d.handleRPC(msg) - - err = conn.WriteJSON(response) - if err != nil { - log.Printf("Write error: %v", err) - break - } - } -} - -func (d *ServerAdapter) handleRPC(req RPCMessage) RPCMessage { - response := RPCMessage{ID: req.ID} - - handler, exists := d.handlers[req.Method] - if !exists { - response.Error = &RPCError{ - Code: -32601, - Message: "Method not found", - } - return response - } - - result, err := handler(req.Params) - if err != nil { - response.Error = &RPCError{ - Code: -32000, - Message: err.Error(), - } - } else { - resultBytes, _ := json.Marshal(result) - response.Result = resultBytes - } - - return response -} 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...) +} From 1872f1069aebe52307d79c656d25117ee2561bfe Mon Sep 17 00:00:00 2001 From: Tyr Mactire Date: Tue, 20 May 2025 16:13:33 -0700 Subject: [PATCH 09/15] http server --- internal/http/apiv1/api.go | 30 ++++++++++++++++++++++++++++++ internal/http/apiv1/entity.go | 13 +++++++++++++ internal/http/apiv1/graph.go | 8 ++++++++ internal/http/apiv1/node.go | 13 +++++++++++++ internal/http/apiv1/observation.go | 13 +++++++++++++ internal/http/apiv1/relation.go | 13 +++++++++++++ internal/http/apiv1/trace.go | 9 +++++++++ internal/http/module.go | 7 +++++++ internal/http/path/paths.go | 13 +++++++++++++ internal/models/models.go | 2 +- 10 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 internal/http/apiv1/api.go create mode 100644 internal/http/apiv1/entity.go create mode 100644 internal/http/apiv1/graph.go create mode 100644 internal/http/apiv1/node.go create mode 100644 internal/http/apiv1/observation.go create mode 100644 internal/http/apiv1/relation.go create mode 100644 internal/http/apiv1/trace.go create mode 100644 internal/http/module.go create mode 100644 internal/http/path/paths.go diff --git a/internal/http/apiv1/api.go b/internal/http/apiv1/api.go new file mode 100644 index 0000000..42c40e1 --- /dev/null +++ b/internal/http/apiv1/api.go @@ -0,0 +1,30 @@ +package apiv1 + +import ( + 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 +} + +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..51e28ab --- /dev/null +++ b/internal/http/apiv1/entity.go @@ -0,0 +1,13 @@ +package apiv1 + +import "github.com/gin-gonic/gin" + +func (a *API) entityPOST(c *gin.Context) { + ctx, span := tracer.Start(c, "entityPOST", tracerAttrs...) + defer span.End() +} + +func (a *API) entityDELETE(c *gin.Context) { + ctx, span := tracer.Start(c, "entityDELETE", tracerAttrs...) + defer span.End() +} diff --git a/internal/http/apiv1/graph.go b/internal/http/apiv1/graph.go new file mode 100644 index 0000000..d49aa40 --- /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) { + ctx, 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..63f3aeb --- /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) { + ctx, span := tracer.Start(c, "nodeGET", tracerAttrs...) + defer span.End() +} + +func (a *API) nodeSearchGET(c *gin.Context) { + ctx, 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..18edee2 --- /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) { + ctx, span := tracer.Start(c, "observationPOST", tracerAttrs...) + defer span.End() +} + +func (a *API) observationDELETE(c *gin.Context) { + ctx, 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..d115b48 --- /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) { + ctx, span := tracer.Start(c, "relationPOST", tracerAttrs...) + defer span.End() +} + +func (a *API) relationDELETE(c *gin.Context) { + ctx, 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..9616d04 --- /dev/null +++ b/internal/http/path/paths.go @@ -0,0 +1,13 @@ +package path + +const ( + Base = "/apiv1" + V1 = Base + "/v1" + + Entities = "/entities" + Graph = "/graph" + Observations = "/observations" + Relations = "/relations" + Nodes = "/nodes" + NodesSearch = Nodes + "/search" +) diff --git a/internal/models/models.go b/internal/models/models.go index aef2ccc..ae37c51 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -1,3 +1,3 @@ package models -// Package api contains the data api for the application. +// Package apiv1 contains the data apiv1 for the application. From d2429f19f467267abbbe18678599473dda72eab9 Mon Sep 17 00:00:00 2001 From: Tyr Mactire Date: Tue, 20 May 2025 17:46:07 -0700 Subject: [PATCH 10/15] progress --- .golangci.yml | 6 +++++- cmd/mcp_dbmem/action/server/server.go | 13 ++++++++++++- go.mod | 6 +----- go.sum | 4 ++-- internal/http/apiv1/api.go | 6 ++++++ internal/http/apiv1/entity.go | 22 +++++++++++++++++++--- internal/http/path/paths.go | 2 +- pkg/api/entity.go | 7 +++++++ pkg/api/graph.go | 6 ++++++ pkg/api/relation.go | 7 +++++++ 10 files changed, 66 insertions(+), 13 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index f5a3795..78f1f4a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -90,6 +90,11 @@ linters: - whitespace settings: + + gomoddirectives: + replace-allow-list: + - github.com/sirupsen/logrus + ireturn: allow: - error @@ -104,6 +109,5 @@ linters: ignore-names: - up - run: issues-exit-code: 1 diff --git a/cmd/mcp_dbmem/action/server/server.go b/cmd/mcp_dbmem/action/server/server.go index 5461ea6..490d975 100644 --- a/cmd/mcp_dbmem/action/server/server.go +++ b/cmd/mcp_dbmem/action/server/server.go @@ -12,6 +12,7 @@ import ( "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" @@ -68,14 +69,24 @@ var Server action.Action = func(ctx context.Context, _ []string) error { httpServer, err := http.NewServer(ctx, http.ServerConfig{ Logic: logic, ApplicationName: config.ApplicationName, - HttpBind: "", + 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) diff --git a/go.mod b/go.mod index e6d6685..61782a8 100644 --- a/go.mod +++ b/go.mod @@ -3,17 +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.0 + 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 diff --git a/go.sum b/go.sum index 01c0d5b..10592d1 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,8 @@ github.com/gin-contrib/sessions v1.0.3 h1:AZ4j0AalLsGqdrKNbbrKcXx9OJZqViirvNGsJT 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.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= -github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +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= diff --git a/internal/http/apiv1/api.go b/internal/http/apiv1/api.go index 42c40e1..e658ea4 100644 --- a/internal/http/apiv1/api.go +++ b/internal/http/apiv1/api.go @@ -10,6 +10,12 @@ type API struct { logic logic.Logic } +func New(logic logic.Logic) *API { + return &API{ + logic: logic, + } +} + func (a *API) Name() string { return "apiv1" } diff --git a/internal/http/apiv1/entity.go b/internal/http/apiv1/entity.go index 51e28ab..a663dde 100644 --- a/internal/http/apiv1/entity.go +++ b/internal/http/apiv1/entity.go @@ -1,13 +1,29 @@ package apiv1 -import "github.com/gin-gonic/gin" +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...) + _, span := tracer.Start(c, "entityPOST", tracerAttrs...) defer span.End() + + var entity api.Entity + // Bind the JSON body to your struct + if err := c.ShouldBindJSON(&entity); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusNotImplemented, struct{}{}) } func (a *API) entityDELETE(c *gin.Context) { - ctx, span := tracer.Start(c, "entityDELETE", tracerAttrs...) + _, span := tracer.Start(c, "entityDELETE", tracerAttrs...) defer span.End() + + c.JSON(http.StatusNotImplemented, struct{}{}) } diff --git a/internal/http/path/paths.go b/internal/http/path/paths.go index 9616d04..cba5440 100644 --- a/internal/http/path/paths.go +++ b/internal/http/path/paths.go @@ -1,7 +1,7 @@ package path const ( - Base = "/apiv1" + Base = "/api" V1 = Base + "/v1" Entities = "/entities" diff --git a/pkg/api/entity.go b/pkg/api/entity.go index be51289..ad9ef3f 100644 --- a/pkg/api/entity.go +++ b/pkg/api/entity.go @@ -1,5 +1,12 @@ 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 { } diff --git a/pkg/api/graph.go b/pkg/api/graph.go index c6a2918..650cb73 100644 --- a/pkg/api/graph.go +++ b/pkg/api/graph.go @@ -1,5 +1,11 @@ package api +// KnowledgeGraph represents the entire knowledge graph. +type KnowledgeGraph struct { + Entities []Entity `json:"entities"` + Relations []Relation `json:"relations"` +} + type ReadGraphRequest struct { } diff --git a/pkg/api/relation.go b/pkg/api/relation.go index 14ea1de..82d6f08 100644 --- a/pkg/api/relation.go +++ b/pkg/api/relation.go @@ -1,5 +1,12 @@ 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 { } From 1b5d5628fc03011bc1f88566c2732df716109717 Mon Sep 17 00:00:00 2001 From: Tyr Mactire Date: Tue, 20 May 2025 22:19:28 -0700 Subject: [PATCH 11/15] progress --- internal/http/apiv1/api.go | 7 ++-- internal/http/apiv1/graph.go | 2 +- internal/http/apiv1/node.go | 4 +-- internal/http/apiv1/observation.go | 4 +-- internal/http/apiv1/relation.go | 4 +-- pkg/api/entity.go | 4 +++ pkg/api/entity_test.go | 54 ++++++++++++++++++++++++++++++ pkg/api/graph.go | 1 + pkg/api/observation.go | 7 ++++ pkg/api/relation.go | 1 + 10 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 pkg/api/entity_test.go diff --git a/internal/http/apiv1/api.go b/internal/http/apiv1/api.go index e658ea4..613ba44 100644 --- a/internal/http/apiv1/api.go +++ b/internal/http/apiv1/api.go @@ -1,18 +1,21 @@ 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 + logic logic.Logic + validate *validator.Validate } func New(logic logic.Logic) *API { return &API{ - logic: logic, + logic: logic, + validate: validator.New(validator.WithRequiredStructEnabled()), } } diff --git a/internal/http/apiv1/graph.go b/internal/http/apiv1/graph.go index d49aa40..dba94e2 100644 --- a/internal/http/apiv1/graph.go +++ b/internal/http/apiv1/graph.go @@ -3,6 +3,6 @@ package apiv1 import "github.com/gin-gonic/gin" func (a *API) graphGET(c *gin.Context) { - ctx, span := tracer.Start(c, "graphGET", tracerAttrs...) + _, span := tracer.Start(c, "graphGET", tracerAttrs...) defer span.End() } diff --git a/internal/http/apiv1/node.go b/internal/http/apiv1/node.go index 63f3aeb..3aa1d32 100644 --- a/internal/http/apiv1/node.go +++ b/internal/http/apiv1/node.go @@ -3,11 +3,11 @@ package apiv1 import "github.com/gin-gonic/gin" func (a *API) nodeGET(c *gin.Context) { - ctx, span := tracer.Start(c, "nodeGET", tracerAttrs...) + _, span := tracer.Start(c, "nodeGET", tracerAttrs...) defer span.End() } func (a *API) nodeSearchGET(c *gin.Context) { - ctx, span := tracer.Start(c, "nodeSearchGET", tracerAttrs...) + _, span := tracer.Start(c, "nodeSearchGET", tracerAttrs...) defer span.End() } diff --git a/internal/http/apiv1/observation.go b/internal/http/apiv1/observation.go index 18edee2..2810b9b 100644 --- a/internal/http/apiv1/observation.go +++ b/internal/http/apiv1/observation.go @@ -3,11 +3,11 @@ package apiv1 import "github.com/gin-gonic/gin" func (a *API) observationPOST(c *gin.Context) { - ctx, span := tracer.Start(c, "observationPOST", tracerAttrs...) + _, span := tracer.Start(c, "observationPOST", tracerAttrs...) defer span.End() } func (a *API) observationDELETE(c *gin.Context) { - ctx, span := tracer.Start(c, "observationDELETE", tracerAttrs...) + _, span := tracer.Start(c, "observationDELETE", tracerAttrs...) defer span.End() } diff --git a/internal/http/apiv1/relation.go b/internal/http/apiv1/relation.go index d115b48..6d8edae 100644 --- a/internal/http/apiv1/relation.go +++ b/internal/http/apiv1/relation.go @@ -3,11 +3,11 @@ package apiv1 import "github.com/gin-gonic/gin" func (a *API) relationPOST(c *gin.Context) { - ctx, span := tracer.Start(c, "relationPOST", tracerAttrs...) + _, span := tracer.Start(c, "relationPOST", tracerAttrs...) defer span.End() } func (a *API) relationDELETE(c *gin.Context) { - ctx, span := tracer.Start(c, "relationDELETE", tracerAttrs...) + _, span := tracer.Start(c, "relationDELETE", tracerAttrs...) defer span.End() } diff --git a/pkg/api/entity.go b/pkg/api/entity.go index ad9ef3f..b0e1916 100644 --- a/pkg/api/entity.go +++ b/pkg/api/entity.go @@ -8,13 +8,17 @@ type Entity struct { } 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..9133e2a --- /dev/null +++ b/pkg/api/entity_test.go @@ -0,0 +1,54 @@ +package api + +import ( + "errors" + "fmt" + "github.com/go-playground/validator/v10" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestEntityValidation(t *testing.T) { + tests := []struct { + name string + input Entity + expectErr bool + error error + }{ + { + name: "empty entity", + input: Entity{}, + expectErr: true, + }, + } + + validate := validator.New(validator.WithRequiredStructEnabled()) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validate.Struct(tt.input) + if tt.expectErr { + assert.Error(t, err) + assert.Equal(t, tt.error, err) + + var validateErrs validator.ValidationErrors + if errors.As(err, &validateErrs) { + for _, e := range validateErrs { + fmt.Println(e.Namespace()) + fmt.Println(e.Field()) + fmt.Println(e.StructNamespace()) + fmt.Println(e.StructField()) + fmt.Println(e.Tag()) + fmt.Println(e.ActualTag()) + fmt.Println(e.Kind()) + fmt.Println(e.Type()) + fmt.Println(e.Value()) + fmt.Println(e.Param()) + fmt.Println() + } + } + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/api/graph.go b/pkg/api/graph.go index 650cb73..eccb92a 100644 --- a/pkg/api/graph.go +++ b/pkg/api/graph.go @@ -10,4 +10,5 @@ type ReadGraphRequest struct { } type ReadGraphResponse struct { + KnowledgeGraph KnowledgeGraph `json:"knowledgeGraph"` } diff --git a/pkg/api/observation.go b/pkg/api/observation.go index 23d936a..efc1bd1 100644 --- a/pkg/api/observation.go +++ b/pkg/api/observation.go @@ -1,6 +1,12 @@ 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 { @@ -10,4 +16,5 @@ type DeleteObservationsRequest struct { } type DeleteObservationsResponse struct { + Success bool `json:"success"` } diff --git a/pkg/api/relation.go b/pkg/api/relation.go index 82d6f08..f46463a 100644 --- a/pkg/api/relation.go +++ b/pkg/api/relation.go @@ -17,4 +17,5 @@ type DeleteRelationsRequest struct { } type DeleteRelationsResponse struct { + Success bool `json:"success"` } From 1a855d45961a5af9a9111a02a151d021bf5ecd67 Mon Sep 17 00:00:00 2001 From: Tyr Mactire Date: Tue, 20 May 2025 23:14:19 -0700 Subject: [PATCH 12/15] lint --- .golangci.yml | 2 ++ pkg/api/entity_test.go | 2 ++ pkg/api/observation.go | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 78f1f4a..590397d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -106,6 +106,8 @@ linters: - 'internal\\config\\.+\.go' varnamelen: + ignore-decls: + - c *gin.Context ignore-names: - up diff --git a/pkg/api/entity_test.go b/pkg/api/entity_test.go index 9133e2a..fad84d5 100644 --- a/pkg/api/entity_test.go +++ b/pkg/api/entity_test.go @@ -9,6 +9,7 @@ import ( ) func TestEntityValidation(t *testing.T) { + t.Parallel() tests := []struct { name string input Entity @@ -25,6 +26,7 @@ func TestEntityValidation(t *testing.T) { validate := validator.New(validator.WithRequiredStructEnabled()) for _, 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) diff --git a/pkg/api/observation.go b/pkg/api/observation.go index efc1bd1..974270f 100644 --- a/pkg/api/observation.go +++ b/pkg/api/observation.go @@ -3,10 +3,10 @@ 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"` + Contents []string `json:"contents" validate:"required,min=1"` } type AddObservationsRequest struct { - Observations []AddObservation `json:"observations" validate:"required,min=1"` + Observations []AddObservation `json:"observations" validate:"required,min=1"` } type AddObservationsResponse struct { From b38f7f077e7b26afef42788e3a132d238709923e Mon Sep 17 00:00:00 2001 From: Tyr Mactire Date: Wed, 21 May 2025 15:55:50 -0700 Subject: [PATCH 13/15] comments --- internal/db/db.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) 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 } From 2a198f901984c82a7c383172f2d95cde014c4d5b Mon Sep 17 00:00:00 2001 From: Tyr Mactire Date: Wed, 21 May 2025 16:32:53 -0700 Subject: [PATCH 14/15] progress --- internal/adapter/adapter.go | 6 ++++ internal/adapter/direct.go | 22 +++++------- internal/db/bun/entity.go | 30 +++++++++++++++- internal/db/bun/observation.go | 32 ++++++++--------- internal/db/bun/relation.go | 35 +++++++++---------- internal/http/apiv1/entity.go | 20 +++++++++-- internal/logic/logic.go | 20 +++++++++-- internal/logic/v1/logic.go | 24 ++++++------- pkg/api/entity_test.go | 64 +++++++++++++++++++++++----------- pkg/api/graph_test.go | 1 + 10 files changed, 169 insertions(+), 85 deletions(-) create mode 100644 pkg/api/graph_test.go diff --git a/internal/adapter/adapter.go b/internal/adapter/adapter.go index 8a39a63..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) diff --git a/internal/adapter/direct.go b/internal/adapter/direct.go index f22c1ef..cdfa48a 100644 --- a/internal/adapter/direct.go +++ b/internal/adapter/direct.go @@ -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) { @@ -382,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..2b54440 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,7 +23,34 @@ 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) + 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) + } + + tx.NewDelete(). + Model(&models.Relation{}). + WherePK() + + err = c.delete(ctx, entity) span.RecordError(err) return err } 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/http/apiv1/entity.go b/internal/http/apiv1/entity.go index a663dde..d81996d 100644 --- a/internal/http/apiv1/entity.go +++ b/internal/http/apiv1/entity.go @@ -8,16 +8,32 @@ import ( ) func (a *API) entityPOST(c *gin.Context) { - _, span := tracer.Start(c, "entityPOST", tracerAttrs...) + ctx, span := tracer.Start(c, "entityPOST", tracerAttrs...) defer span.End() - var entity api.Entity + 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{}{}) } 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/logic.go b/internal/logic/v1/logic.go index 4b7332e..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...) diff --git a/pkg/api/entity_test.go b/pkg/api/entity_test.go index fad84d5..9456aaf 100644 --- a/pkg/api/entity_test.go +++ b/pkg/api/entity_test.go @@ -2,51 +2,75 @@ package api import ( "errors" - "fmt" + "testing" + "github.com/go-playground/validator/v10" "github.com/stretchr/testify/assert" - "testing" ) -func TestEntityValidation(t *testing.T) { +func testEntityValidation(t *testing.T) { t.Parallel() tests := []struct { name string - input Entity + input *Entity expectErr bool - error error + errors []struct { + Namespace string + Tag string + } }{ { - name: "empty entity", - input: Entity{}, + 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 _, tt := range tests { + 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) - assert.Equal(t, tt.error, err) var validateErrs validator.ValidationErrors if errors.As(err, &validateErrs) { + assert.Equal(t, 0, len(validateErrs)) for _, e := range validateErrs { - fmt.Println(e.Namespace()) - fmt.Println(e.Field()) - fmt.Println(e.StructNamespace()) - fmt.Println(e.StructField()) - fmt.Println(e.Tag()) - fmt.Println(e.ActualTag()) - fmt.Println(e.Kind()) - fmt.Println(e.Type()) - fmt.Println(e.Value()) - fmt.Println(e.Param()) - fmt.Println() + 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_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 From 71d05b7bd7cbc6a80e99573ea4a5d9601ba7c631 Mon Sep 17 00:00:00 2001 From: Tyr Mactire Date: Wed, 21 May 2025 23:35:20 -0700 Subject: [PATCH 15/15] transactions --- internal/db/bun/entity.go | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/internal/db/bun/entity.go b/internal/db/bun/entity.go index 2b54440..3494e62 100644 --- a/internal/db/bun/entity.go +++ b/internal/db/bun/entity.go @@ -46,13 +46,24 @@ func (c *Client) DeleteEntity(ctx context.Context, entity *models.Entity) db.Err return c.ProcessError(err) } - tx.NewDelete(). - Model(&models.Relation{}). + 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) + } - err = c.delete(ctx, entity) - span.RecordError(err) - return 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) {