Geospatial analyst agent built with the Anthropic Agent SDK.
Lava Stew shows how to deploy the Claude Agent SDK in a stateful, production-ready architecture.
- Stateful Agent Worker Pattern: Long-running containerized processes maintain Agent SDK session state in memory
- Python Tools via TypeScript: TypeScript infrastructure invoking Python geospatial scripts
- SSE Streaming: Real-time response streaming from agent to client
- Custom MCP Tools: Geocoding and distance calculation tools wrapped for the Agent SDK
Client (curl/Flutter/React)
↓ HTTP POST
API Server (Express on port 3001)
↓ Publish to chat.requests queue
RabbitMQ (port 5672, management UI on 15672)
↓ Consume from queue
Agent Worker Process (Anthropic SDK)
→ Python tools (geocoding, distance via uv)
↓ Publish events to reply queue
RabbitMQ
↓ Consume from reply queue
API Server converts to SSE
↓ SSE stream back to client
The RabbitMQ RPC pattern uses exclusive reply queues per request that auto-delete on disconnect.
- Node.js 20+
- Python 3.11+ with uv (
curl -LsSf https://astral.sh/uv/install.sh | sh) - Anthropic API key (from Anthropic Console)
- Google Maps API key (from Google Cloud Console)
- Docker and Docker Compose (optional, for containerized deployment)
Copy the example environment file and add your API keys:
cp .env.example .env
# Edit .env and add your ANTHROPIC_API_KEY and GOOGLE_MAPS_API_KEYYour .env file should contain:
ANTHROPIC_API_KEY=sk-ant-...
GOOGLE_MAPS_API_KEY=...
API_SERVER_PORT=3001
RABBITMQ_URL=amqp://lava:stew@localhost:5672# Install Node.js dependencies
npm install
# Python dependencies are managed by uv and installed on-demand# Start all services (RabbitMQ, API server, Agent Worker)
docker compose up -d
# View logs
docker compose logs -f
# Stop services
docker compose downYou should see:
[API] Connected to RabbitMQ at amqp://lava:stew@rabbitmq:5672
[API] Server listening on port 3001
[WORKER] Connected to RabbitMQ at amqp://lava:stew@rabbitmq:5672
[WORKER] Listening on queue 'chat.requests'
[WORKER] Worker ready to process messages
Access the RabbitMQ management interface at http://localhost:15672 (login: lava/stew) to observe message flow.
curl -X POST http://localhost:3001/chat \
-H "Content-Type: application/json" \
-d '{"conversationId": "test-123", "message": "What is the distance between Seattle and Portland?"}'Expected output: SSE stream showing the agent:
- Geocoding Seattle →
{"lat": 47.6061389, "lng": -122.3328481, ...} - Geocoding Portland →
{"lat": 45.515232, "lng": -122.6783853, ...} - Calculating distance →
{"distance_km": 233.93, "distance_miles": 145.36} - Responding with natural language answer
Two client applications are provided. You only need to run one — they connect to the same backend.
cd react_client
pnpm install
pnpm devOpen http://localhost:5173 in your browser. The React client uses MapLibre GL JS with deck.gl for GPU-accelerated map rendering, and the BLoC pattern for state management — see react_client/README.md for details.
cd flutter_client
flutter pub get
flutter runSelect your target platform when prompted. The Flutter client also uses BLoC via flutter_bloc.
Both clients provide:
- Two-pane layout (chat + map)
- SSE streaming for real-time responses
- Map visualization of geocoded locations and isochrones
- Markdown rendering in chat responses (including tables)
The agent worker logs show tool invocations with timing:
[TOOL] test-123 | geocode | {"location":"Seattle, WA"} | {...} | 607ms
[TOOL] test-123 | geocode | {"location":"Portland, Oregon"} | {...} | 486ms
[TOOL] test-123 | calculate_distance | {...} | {...} | 146ms
For local development without Docker, start RabbitMQ first:
# Start RabbitMQ only
docker compose up rabbitmq -d
# In separate terminals:
cd api_server && npm run dev
cd agent_worker && npm run devlava_stew/
├── api_server/
│ └── src/
│ └── server.ts # API server with SSE streaming
├── agent_worker/
│ ├── src/
│ │ ├── server.ts # Agent worker process with SDK integration
│ │ ├── mcpServer.ts # MCP server wrapper for tools
│ │ ├── tools.ts # Tool schema definitions
│ │ └── executor.ts # Python tool execution
│ └── scripts/
│ ├── geocode.py # Google Maps geocoding
│ └── calculate_distance.py # Geodesic distance calculation
├── flutter_client/ # Flutter client (desktop/mobile)
├── react_client/ # React client (web)
├── docker-compose.yml # Container orchestration with RabbitMQ
└── .env # Environment variables
- Basic distance query:
curl -X POST http://localhost:3001/chat \
-H "Content-Type: application/json" \
-d '{"conversationId": "test-1", "message": "What is the distance between Seattle and Portland?"}'- Single geocoding:
curl -X POST http://localhost:3001/chat \
-H "Content-Type: application/json" \
-d '{"conversationId": "test-2", "message": "Where is San Francisco?"}'- Conversation continuity (same conversationId):
curl -X POST http://localhost:3001/chat \
-H "Content-Type: application/json" \
-d '{"conversationId": "test-3", "message": "Where is Seattle?"}'
curl -X POST http://localhost:3001/chat \
-H "Content-Type: application/json" \
-d '{"conversationId": "test-3", "message": "How far is it from there to Portland?"}'- Input:
{ location: string } - Output:
{ lat: number, lng: number, formatted_address: string } - Example:
"Seattle, WA"→{"lat": 47.6061, "lng": -122.3328, ...}
- Input:
{ point1: {lat, lng}, point2: {lat, lng} } - Output:
{ distance_km: number, distance_miles: number } - Example: Seattle to Portland →
{"distance_km": 233.93, "distance_miles": 145.36}
This is a demonstration implementation. Known limitations:
- No session persistence: Agent worker restart loses all conversation history
- Single agent worker: No load balancing or high availability
- No database: Tool results logged to stdout only, not persisted
- No authentication: Open endpoint
- Memory unbounded: Session map grows without eviction
Test Python tools directly:
# Test geocoding
cd scripts
uv run python geocode.py "Seattle, WA"
# Test distance calculation (Seattle to Portland coordinates)
uv run python calculate_distance.py "47.6061,-122.3328" "45.5152,-122.6784"# Check API server
curl http://localhost:3001/healthISC