package tools import ( "context" "strings" "github.com/google/uuid" "github.com/modelcontextprotocol/go-sdk/mcp" "git.warky.dev/wdevs/amcs/internal/session" "git.warky.dev/wdevs/amcs/internal/store" ext "git.warky.dev/wdevs/amcs/internal/types" ) type ChatHistoryTool struct { store *store.DB sessions *session.ActiveProjects } func NewChatHistoryTool(db *store.DB, sessions *session.ActiveProjects) *ChatHistoryTool { return &ChatHistoryTool{store: db, sessions: sessions} } // save_chat_history type SaveChatHistoryInput struct { SessionID string `json:"session_id" jsonschema:"unique identifier for the chat session (e.g. OpenClaw session id or thread id)"` Title string `json:"title,omitempty" jsonschema:"optional human-readable title for this conversation"` Channel string `json:"channel,omitempty" jsonschema:"optional channel name (e.g. telegram, discord, signal)"` AgentID string `json:"agent_id,omitempty" jsonschema:"optional agent identifier (e.g. claude, codex, main)"` Project string `json:"project,omitempty" jsonschema:"optional project name or id; falls back to active session project"` Messages []ext.ChatMessage `json:"messages" jsonschema:"ordered list of messages in the conversation"` Summary string `json:"summary,omitempty" jsonschema:"optional brief summary of the conversation"` Metadata map[string]any `json:"metadata,omitempty" jsonschema:"optional arbitrary key-value metadata"` } type SaveChatHistoryOutput struct { ChatHistory ext.ChatHistory `json:"chat_history"` } func (t *ChatHistoryTool) SaveChatHistory(ctx context.Context, req *mcp.CallToolRequest, in SaveChatHistoryInput) (*mcp.CallToolResult, SaveChatHistoryOutput, error) { if strings.TrimSpace(in.SessionID) == "" { return nil, SaveChatHistoryOutput{}, errRequiredField("session_id") } if len(in.Messages) == 0 { return nil, SaveChatHistoryOutput{}, errRequiredField("messages") } project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, false) if err != nil { return nil, SaveChatHistoryOutput{}, err } h := ext.ChatHistory{ SessionID: strings.TrimSpace(in.SessionID), Title: strings.TrimSpace(in.Title), Channel: strings.TrimSpace(in.Channel), AgentID: strings.TrimSpace(in.AgentID), Messages: in.Messages, Summary: strings.TrimSpace(in.Summary), Metadata: in.Metadata, } if h.Metadata == nil { h.Metadata = map[string]any{} } if project != nil { h.ProjectID = &project.ID } saved, err := t.store.SaveChatHistory(ctx, h) if err != nil { return nil, SaveChatHistoryOutput{}, err } return nil, SaveChatHistoryOutput{ChatHistory: saved}, nil } // get_chat_history type GetChatHistoryInput struct { ID string `json:"id,omitempty" jsonschema:"UUID of the saved chat history"` SessionID string `json:"session_id,omitempty" jsonschema:"original session_id — returns the most recent history for that session"` } type GetChatHistoryOutput struct { ChatHistory *ext.ChatHistory `json:"chat_history"` } func (t *ChatHistoryTool) GetChatHistory(ctx context.Context, _ *mcp.CallToolRequest, in GetChatHistoryInput) (*mcp.CallToolResult, GetChatHistoryOutput, error) { if in.ID == "" && in.SessionID == "" { return nil, GetChatHistoryOutput{}, errRequiredField("id or session_id") } if in.ID != "" { id, err := uuid.Parse(in.ID) if err != nil { return nil, GetChatHistoryOutput{}, errInvalidField("id", "invalid id", "must be a valid UUID") } h, found, err := t.store.GetChatHistory(ctx, id) if err != nil { return nil, GetChatHistoryOutput{}, err } if !found { return nil, GetChatHistoryOutput{}, nil } return nil, GetChatHistoryOutput{ChatHistory: &h}, nil } h, found, err := t.store.GetChatHistoryBySessionID(ctx, in.SessionID) if err != nil { return nil, GetChatHistoryOutput{}, err } if !found { return nil, GetChatHistoryOutput{}, nil } return nil, GetChatHistoryOutput{ChatHistory: &h}, nil } // list_chat_histories type ListChatHistoriesInput struct { Project string `json:"project,omitempty" jsonschema:"filter by project name or id"` Channel string `json:"channel,omitempty" jsonschema:"filter by channel (e.g. telegram, discord)"` AgentID string `json:"agent_id,omitempty" jsonschema:"filter by agent id"` SessionID string `json:"session_id,omitempty" jsonschema:"filter by session_id"` Days int `json:"days,omitempty" jsonschema:"only include histories from the last N days"` Limit int `json:"limit,omitempty" jsonschema:"maximum number of results to return (default 20, max 200)"` } type ListChatHistoriesOutput struct { ChatHistories []ext.ChatHistory `json:"chat_histories"` } func (t *ChatHistoryTool) ListChatHistories(ctx context.Context, req *mcp.CallToolRequest, in ListChatHistoriesInput) (*mcp.CallToolResult, ListChatHistoriesOutput, error) { filter := store.ListChatHistoriesFilter{ Channel: strings.TrimSpace(in.Channel), AgentID: strings.TrimSpace(in.AgentID), SessionID: strings.TrimSpace(in.SessionID), Days: in.Days, Limit: in.Limit, } if in.Project != "" { project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, false) if err != nil { return nil, ListChatHistoriesOutput{}, err } if project != nil { filter.ProjectID = &project.ID } } histories, err := t.store.ListChatHistories(ctx, filter) if err != nil { return nil, ListChatHistoriesOutput{}, err } if histories == nil { histories = []ext.ChatHistory{} } return nil, ListChatHistoriesOutput{ChatHistories: histories}, nil } // delete_chat_history type DeleteChatHistoryInput struct { ID uuid.UUID `json:"id" jsonschema:"UUID of the chat history to delete"` } type DeleteChatHistoryOutput struct { Deleted bool `json:"deleted"` } func (t *ChatHistoryTool) DeleteChatHistory(ctx context.Context, _ *mcp.CallToolRequest, in DeleteChatHistoryInput) (*mcp.CallToolResult, DeleteChatHistoryOutput, error) { deleted, err := t.store.DeleteChatHistory(ctx, in.ID) if err != nil { return nil, DeleteChatHistoryOutput{}, err } return nil, DeleteChatHistoryOutput{Deleted: deleted}, nil }