for assorted things
1#!/usr/bin/env -S uv run --script --quiet
2# /// script
3# requires-python = ">=3.12"
4# dependencies = ["pydantic-ai", "pydantic-settings", "rich"]
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 [`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 `ANTHROPIC_API_KEY` and defaults to claude-sonnet-4-5, but you can set `AI_MODEL` in `.env` to use a different model
20"""
21
22import argparse
23import asyncio
24from pydantic_settings import BaseSettings, SettingsConfigDict
25from pydantic import Field
26from pydantic_ai import Agent
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(env_file=".env", extra="ignore")
36
37 hue_bridge_ip: str = Field(default=...)
38 hue_bridge_username: str = Field(default=...)
39 anthropic_api_key: str | None = Field(default=None)
40
41 ai_model: KnownModelName = Field(default="anthropic:claude-sonnet-4-5")
42
43
44settings = Settings()
45console = Console()
46
47async def run_agent():
48 hub_mcp = MCPServerStdio(
49 command="uvx",
50 args=[
51 "smart-home@git+https://github.com/jlowin/fastmcp.git#subdirectory=examples/smart_home"
52 ],
53 env={
54 "HUE_BRIDGE_IP": settings.hue_bridge_ip,
55 "HUE_BRIDGE_USERNAME": settings.hue_bridge_username,
56 },
57 )
58
59 agent = Agent(
60 settings.ai_model,
61 mcp_servers=[hub_mcp],
62 )
63
64 console.print(
65 Panel.fit(
66 f"[bold cyan]🏠 lights agent[/bold cyan]\n"
67 f"[dim]model: {settings.ai_model}[/dim]",
68 border_style="blue",
69 )
70 )
71
72 parser = argparse.ArgumentParser(description="Send a command to the lights agent.")
73 parser.add_argument(
74 "--message",
75 "-m",
76 type=str,
77 default="soft and dim - Jessica Pratt energy, all areas",
78 help="The message to send to the agent (defaults to 'soft and dim - Jessica Pratt energy, all areas').",
79 )
80 parser.add_argument(
81 "--once",
82 action="store_true",
83 help="Run once and exit instead of entering interactive mode.",
84 )
85 args = parser.parse_args()
86
87 console.print(f"\n[bold yellow]→[/bold yellow] {args.message}")
88 result = await agent.run(args.message)
89 console.print(f"[dim green]{result.output}[/dim green]")
90
91 if not args.once:
92 while True:
93 try:
94 user_input = Prompt.ask("\n[bold green]enter a message[/bold green]")
95 console.print(f"[bold yellow]→[/bold yellow] {user_input}")
96 result = await agent.run(user_input)
97 console.print(f"[dim green]{result.output}[/dim green]")
98 except KeyboardInterrupt:
99 console.print("\n[dim red]exiting...[/dim red]")
100 break
101
102
103if __name__ == "__main__":
104 import os
105
106 if settings.anthropic_api_key:
107 os.environ["ANTHROPIC_API_KEY"] = settings.anthropic_api_key
108
109 asyncio.run(run_agent())