From Dashboard to Assistant: Giving My Life Hub 151 Tools and a Memory

How I rebuilt my life dashboard's chat from a basic Haiku chatbot with 17 hardcoded tools into a full personal assistant with 151 MCP tools, conversation memory, and dual LLM provider support.

From Dashboard to Assistant: Giving My Life Hub 151 Tools and a Memory

My personal life dashboard, Life Hub, tracks everything: mood, sleep, habits, goals, journal entries, health metrics, finances, social interactions, screen time, and more. It has 33 pages, 38 database models, 15 data collectors, and 151 MCP tools.

But the chat feature was an afterthought. Claude Haiku with 17 hardcoded read-only tools, no conversation memory, and a response cap of 1024 tokens. It could answer "how much screen time today?" but couldn't log a mood score, create a goal, or remember what you asked five minutes ago.

I wanted Life Hub to stop being a dashboard I check and start being an assistant I talk to. Here's how I rebuilt the chat from the ground up.

The Problem with Hardcoded Tools

The original chat.py had 17 tool schemas defined inline, each one a copy-paste of information that already existed in the MCP server. Every time I added a new MCP tool (and there are now 151 of them), the chat didn't benefit. The 7,000+ lines of MCP tool definitions in mcp_server.py lived in a parallel universe from the chat.

Worse, the tools were all read-only. You could ask "what's my mood?" but you couldn't say "log mood 8." The chat was a query interface, not an agent.

The MCP Tool Bridge

The fix was obvious once I looked at what FastMCP exposes internally. The mcp._tool_manager._tools attribute is a dict[str, Tool] where each Tool has everything I need:

  • .name: the tool name
  • .description: what it does
  • .parameters: full JSON Schema (already compatible with both Anthropic and OpenAI formats)
  • .fn: the async callable that does the work

That's the entire bridge. No copying schemas, no maintaining a parallel registry. One function introspects the MCP server and generates tool schemas for whichever LLM provider you're using:

def _build_tools():
    from app.mcp_server import mcp
    tools = mcp._tool_manager._tools

    for name, tool in tools.items():
        # Anthropic format: {"name", "description", "input_schema"}
        anthropic_schemas.append({
            "name": tool.name,
            "description": tool.description,
            "input_schema": tool.parameters,
        })
        # OpenAI format: {"type": "function", "function": {...}}
        openrouter_schemas.append({
            "type": "function",
            "function": {
                "name": tool.name,
                "description": tool.description,
                "parameters": tool.parameters,
            },
        })
        registry[tool.name] = tool.fn

151 tools, zero lines of duplicated schema. When I add a new MCP tool, the chat gets it for free.

Dual Provider Support

I already had OpenRouter working for the advisor council feature, so I wanted the chat to support both Anthropic's direct API and OpenRouter. Same model (Claude Sonnet), different API shapes.

The trick is a normalized response type:

@dataclass
class ToolCall:
    id: str
    name: str
    arguments: dict

@dataclass
class LLMResponse:
    text: str | None = None
    tool_calls: list[ToolCall] = field(default_factory=list)
    stop_reason: str = "end_turn"

The _call_anthropic() and _call_openrouter() functions each handle their provider's quirks (Anthropic uses tool_use content blocks, OpenRouter uses tool_calls arrays), but both return the same LLMResponse. The agent loop doesn't care which provider is active.

Configuration is two env vars:

CHAT_PROVIDER=anthropic   # or "openrouter"
CHAT_MODEL=               # empty = auto-select per provider

Write Tools and Cache Invalidation

Of the 151 tools, 34 are write operations: log_mood, create_goal, update_project, toggle_milestone, etc. I classify them by prefix:

_WRITE_PREFIXES = (
    "log_", "create_", "update_", "delete_", "set_", "toggle_",
    "link_", "unlink_", "dismiss_", "add_", "run_", "send_",
    "post_", "save_", "publish_", "cleanup_",
)

When the agent calls a write tool, two things happen:

  1. The tool executes (same function the MCP server uses)
  2. The TTL cache is invalidated so the dashboard reflects the change immediately

This means you can say "log mood 7, feeling productive today" and the today page will show the updated mood without a refresh.

Conversation Memory

The old chat was stateless. Each message started fresh. Now every message is persisted to a chat_messages table keyed by conversation_id:

class ChatMessage(Base):
    __tablename__ = "chat_messages"
    id = mapped_column(primary_key=True)
    conversation_id = mapped_column(nullable=False, index=True)
    role = mapped_column(nullable=False)       # user | assistant
    content = mapped_column(nullable=False)
    tools_used = mapped_column(nullable=True)   # JSON list
    tokens_in = mapped_column(nullable=True)
    tokens_out = mapped_column(nullable=True)
    created_at = mapped_column(nullable=False)

The server loads the last 20 messages for context. The client generates a UUID per session and sends it with every request. No conversation history in the POST body. The server handles it.

Ambient Context

Every request includes a lightweight context block in the system prompt (no LLM call needed, just quick DB queries):

Date: Sunday, 2026-03-09
Mood: 7/10
Sleep: 7.5 hours
Habits done today: 3/5 (exercise, reading, meditation)
Priorities: finish blog post, prep for Monday standup
Active goals: 4 (Read 12 books: 42%, Exercise 3x/week: 67%, ...)

The model knows what today looks like before you ask. "How am I doing?" gets a real answer, not "I'd need to check your data first."

The Agent Loop

The loop is straightforward: send message → get response → if tool call, execute and feed result back → repeat until text response or max turns (15). The UI shows which tools were called as subtle badges on each assistant message, so you can see the work happening.

What Changed

BeforeAfter
ModelClaude HaikuClaude Sonnet
Tools17 hardcoded151 auto-bridged
Tool typesRead-onlyRead + Write
MemoryNonePersistent (DB)
ContextNoneAmbient (mood, sleep, habits, goals)
ProvidersAnthropic onlyAnthropic + OpenRouter
Max tokens1,0244,096
Max turns815

What's Next

The chat is the foundation for turning Life Hub into an actual personal assistant. Next steps:

  • Natural language quick capture: "mood 7, slept 8h, ran 3 miles" parsed into structured data
  • Proactive nudges: the assistant notices you haven't logged sleep and asks
  • Actionable notifications: ntfy push notifications with reply buttons for quick logging

The dashboard isn't going away, but the chat is where I'll spend most of my time now.