diff --git a/README.md b/README.md index 18d76d1..b974caf 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,8 @@ make test MCP-сервер (Model Context Protocol) встроен в основной бинарь и доступен как HTTP-эндпоинт `/api/mcp` (streamable HTTP) на REST-gateway — чтобы LLM-клиент (Claude Desktop, IDE с MCP-поддержкой и т.п.) мог играть в «Вечерний детектив». Инструменты вызывают игровые сервисы напрямую, отдельный процесс не нужен. +Ходы через `make_move` сохраняются с `actions.via_mcp = true`; через REST/gRPC — с `false`. Отметку задаёт сервер, клиент её не передаёт. У старых ходов значение `false`: их источник не отслеживался. Это отметка канала MCP, а не гарантия того, кто фактически играет. + Инструменты: | Инструмент | Назначение | diff --git a/bin/evening_detective_server b/bin/evening_detective_server index a3b1683..7e2cb11 100755 Binary files a/bin/evening_detective_server and b/bin/evening_detective_server differ diff --git a/internal/app/server.go b/internal/app/server.go index 50b4709..e5240e0 100644 --- a/internal/app/server.go +++ b/internal/app/server.go @@ -1099,6 +1099,7 @@ func (s *server) AddTeamAction(ctx context.Context, req *proto.AddTeamActionReq) int(req.Id), req.Password, req.Code, + false, ) if err != nil { return &proto.AddTeamActionRsp{ diff --git a/internal/repos/actions_repo/repo.go b/internal/repos/actions_repo/repo.go index 39eb955..cdbdc05 100644 --- a/internal/repos/actions_repo/repo.go +++ b/internal/repos/actions_repo/repo.go @@ -21,15 +21,16 @@ func NewActionsRepo(pool *pgxpool.Pool) *ActionsRepo { } } -func (r *ActionsRepo) AddAction(ctx context.Context, teamId int, code string) (int, error) { +func (r *ActionsRepo) AddAction(ctx context.Context, teamId int, code string, viaMCP bool) (int, error) { id := 0 err := r.pool.QueryRow( ctx, - `INSERT INTO actions (code, team_id) - VALUES ($1, $2) + `INSERT INTO actions (code, team_id, via_mcp) + VALUES ($1, $2, $3) RETURNING id`, code, teamId, + viaMCP, ).Scan(&id) if err != nil { diff --git a/internal/services/game_service/service.go b/internal/services/game_service/service.go index a6fa9c6..c4adfee 100644 --- a/internal/services/game_service/service.go +++ b/internal/services/game_service/service.go @@ -234,7 +234,7 @@ func (s *GameService) GetTeamActions(ctx context.Context, teamId int, password s return s.storyteller.GetStory(scenario.Story, actions), mapGame(game, s.domain), nil } -func (s *GameService) AddTeamAction(ctx context.Context, teamId int, password string, actionCode string) error { +func (s *GameService) AddTeamAction(ctx context.Context, teamId int, password string, actionCode string, viaMCP bool) error { team, err := s.teamsRepo.GetTeamByID(ctx, teamId) if err != nil { return err @@ -244,7 +244,7 @@ func (s *GameService) AddTeamAction(ctx context.Context, teamId int, password st return teams_repo.ErrTeamNotFound } - _, err = s.actionsRepo.AddAction(ctx, teamId, actionCode) + _, err = s.actionsRepo.AddAction(ctx, teamId, actionCode, viaMCP) if err != nil { return err } diff --git a/internal/services/mcp_service/service.go b/internal/services/mcp_service/service.go index a04cb58..1f154fd 100644 --- a/internal/services/mcp_service/service.go +++ b/internal/services/mcp_service/service.go @@ -30,7 +30,7 @@ const defaultCallTimeout = 10 * time.Second // без БД. type GamePlayer interface { GetTeamActions(ctx context.Context, teamId int, password string) (*storytelling.Story, *game_service.Game, error) - AddTeamAction(ctx context.Context, teamId int, password, actionCode string) error + AddTeamAction(ctx context.Context, teamId int, password, actionCode string, viaMCP bool) error SubmitTeamAnswers(ctx context.Context, teamId int, password string, answers []game_service.Answer) error } @@ -261,7 +261,7 @@ func (s *MCPService) handleMakeMove(ctx context.Context, req mcp.CallToolRequest ctx, cancel := context.WithTimeout(ctx, defaultCallTimeout) defer cancel() - if err := s.game.AddTeamAction(ctx, int(teamID), password, code); err != nil { + if err := s.game.AddTeamAction(ctx, int(teamID), password, code, true); err != nil { return mcp.NewToolResultError(fmt.Sprintf("make_move: %v", err)), nil } diff --git a/internal/services/mcp_service/service_test.go b/internal/services/mcp_service/service_test.go index b08690b..35f2861 100644 --- a/internal/services/mcp_service/service_test.go +++ b/internal/services/mcp_service/service_test.go @@ -17,9 +17,10 @@ import ( // fakeGamePlayer — мок GamePlayer с предзаданными историей, игрой и // ошибками (реализует интерфейс GamePlayer, БД не нужна). type fakeGamePlayer struct { - story *storytelling.Story - game *game_service.Game - err error + story *storytelling.Story + game *game_service.Game + err error + viaMCP bool } func (f *fakeGamePlayer) GetTeamActions(_ context.Context, teamID int, password string) (*storytelling.Story, *game_service.Game, error) { @@ -29,7 +30,8 @@ func (f *fakeGamePlayer) GetTeamActions(_ context.Context, teamID int, password return f.story, f.game, nil } -func (f *fakeGamePlayer) AddTeamAction(_ context.Context, teamID int, password, code string) error { +func (f *fakeGamePlayer) AddTeamAction(_ context.Context, teamID int, password, code string, viaMCP bool) error { + f.viaMCP = viaMCP if f.err != nil { return f.err } @@ -295,7 +297,8 @@ func TestGetTeamStoryInvalidTeamID(t *testing.T) { } func TestMakeMove(t *testing.T) { - f := newMCPFixture(t, &fakeGamePlayer{story: defaultStory(), game: defaultGame()}) + player := &fakeGamePlayer{story: defaultStory(), game: defaultGame()} + f := newMCPFixture(t, player) got := f.call(t, "make_move", map[string]any{ "team_id": "10", // строка тоже принимается @@ -305,6 +308,9 @@ func TestMakeMove(t *testing.T) { if !strings.Contains(got, "entrance") { t.Fatalf("make_move: %q; want updated story JSON", got) } + if !player.viaMCP { + t.Fatal("make_move must mark the action as via MCP") + } } func TestMakeMoveActionError(t *testing.T) { diff --git a/migrations/20260905120000_add_via_mcp_to_actions.sql b/migrations/20260905120000_add_via_mcp_to_actions.sql new file mode 100644 index 0000000..5310b0b --- /dev/null +++ b/migrations/20260905120000_add_via_mcp_to_actions.sql @@ -0,0 +1,5 @@ +-- +goose Up +ALTER TABLE actions ADD COLUMN via_mcp BOOLEAN NOT NULL DEFAULT FALSE; + +-- +goose Down +ALTER TABLE actions DROP COLUMN via_mcp;