for assorted things
1#!/usr/bin/env -S uv run --script --quiet
2# /// script
3# requires-python = ">=3.12"
4# dependencies = ["marvin@git+https://github.com/prefecthq/marvin.git"]
5# ///
6"""
7Make some change to my phillips hue network of lights via agent + MCP server.
8
9Usage:
10
11```bash
12./update-lights -m "turn on sahara in the living room and nightlight in the kitchen"
13```
14
15Details:
16- uses a [`marvin`](https://github.com/prefecthq/marvin) (built on [`pydantic-ai`](https://github.com/pydantic/pydantic-ai)) agent
17- the agent spins up a [`fastmcp`](https://github.com/jlowin/fastmcp) MCP server that talks to my [`phue`](https://github.com/studioimaginaire/phue) bridge
18- set `HUE_BRIDGE_IP` and `HUE_BRIDGE_USERNAME` in `.env` or otherwise in environment
19- uses `OPENAI_API_KEY` by default, but you can set `AI_MODEL` in `.env` or otherwise in environment to use a different model
20"""
21
22import marvin
23import argparse
24from pathlib import Path
25from pydantic_settings import BaseSettings, SettingsConfigDict
26from pydantic import Field
27from pydantic_ai.mcp import MCPServerStdio
28from pydantic_ai.models import KnownModelName
29from rich.console import Console
30from rich.panel import Panel
31from rich.prompt import Prompt
32
33
34class Settings(BaseSettings):
35 model_config = SettingsConfigDict(
36 env_file=Path(__file__).parent / ".env", extra="ignore"
37 )
38
39 hue_bridge_ip: str = Field(default=...)
40 hue_bridge_username: str = Field(default=...)
41 anthropic_api_key: str | None = Field(default=None)
42
43 ai_model: KnownModelName = Field(default="anthropic:claude-opus-4-5")
44
45
46settings = Settings()
47console = Console()
48
49hub_mcp = MCPServerStdio(
50 command="uvx",
51 args=[
52 "smart-home@git+https://github.com/jlowin/fastmcp.git#subdirectory=examples/smart_home"
53 ],
54 env={
55 "HUE_BRIDGE_IP": settings.hue_bridge_ip,
56 "HUE_BRIDGE_USERNAME": settings.hue_bridge_username,
57 },
58)
59
60
61if __name__ == "__main__":
62 import os
63
64 if settings.anthropic_api_key:
65 os.environ["ANTHROPIC_API_KEY"] = settings.anthropic_api_key
66
67 parser = argparse.ArgumentParser(description="Send a command to the Marvin agent.")
68 parser.add_argument(
69 "--message",
70 "-m",
71 type=str,
72 default="soft and dim - Jessica Pratt energy, all areas",
73 help="The message to send to the agent (defaults to 'soft and dim - Jessica Pratt energy, all areas').",
74 )
75 parser.add_argument(
76 "--once",
77 action="store_true",
78 help="Run once and exit instead of entering interactive mode.",
79 )
80 args = parser.parse_args()
81
82 agent = marvin.Agent(
83 model=settings.ai_model,
84 mcp_servers=[hub_mcp],
85 )
86
87 console.print(
88 Panel.fit(
89 f"[bold cyan]🏠 lights agent[/bold cyan]\n"
90 f"[dim]model: {settings.ai_model}[/dim]",
91 border_style="blue",
92 )
93 )
94
95 with marvin.Thread():
96 console.print(f"\n[bold yellow]→[/bold yellow] {args.message}")
97 agent.run(str(args.message))
98
99 if not args.once:
100 while True:
101 try:
102 user_input = Prompt.ask(
103 "\n[bold green]enter a message[/bold green]"
104 )
105 console.print(f"[bold yellow]→[/bold yellow] {user_input}")
106 agent.run(str(user_input))
107 except KeyboardInterrupt:
108 console.print("\n[dim red]exiting...[/dim red]")
109 break