MCP Server
The MCP (Model Context Protocol) server exposes Hone's financial data to LLMs for conversational queries. All communication stays on your local network — no cloud services required.
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Local Network │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌──────────────────────────┐ │
│ │ Raspberry Pi │ HTTP │ Mac / Windows │ │
│ │ (hone serve) │◄───────►│ with GPU │ │
│ │ │ │ │ │
│ │ Port 3000: API │ │ Ollama / vLLM │ │
│ │ Port 3001: MCP │ │ + MCP Client │ │
│ └─────────────────┘ └──────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Key benefits:
- Data stays local — All communication on local network
- LLM-agnostic — Works with Claude Desktop, Ollama-based agents, or custom clients
- Read-only — MCP tools only query data, never modify
- Separation of concerns — Pi handles data storage, Mac handles LLM inference
Quick Start
1. Start Hone with MCP enabled
On your Raspberry Pi (or wherever Hone runs):
# Start with MCP on port 3001
# MCP is an OAuth 2.1 resource server (spec 2026-07-28): RFC 9728 metadata
# and RFC 8707 audience-bound JWTs. We do not host an authorization server.
# --mcp-allowed-hosts is needed for access from other machines: the MCP
# server only accepts loopback Host headers by default (DNS rebinding
# protection), so list the hostname/IP clients will use to reach it
export HONE_MCP_RESOURCE=http://pi-hostname:3001/mcp
export HONE_MCP_JWT_SECRET=your-signing-secret
hone serve --port 3000 --mcp-port 3001 --host 0.0.0.0 --mcp-allowed-hosts pi-hostname
# Mint a token bound to that resource (rejected on /api):
export HONE_MCP_KEY=$(hone mcp-token --ttl 3600)
# Or with all your usual options
hone serve \
--port 3000 \
--mcp-port 3001 \
--host 0.0.0.0 \
--mcp-allowed-hosts pi-hostname,192.168.1.50 \
--static-dir /path/to/ui/dist
You should see:
🚀 Starting Hone web server...
Database: hone.db
Listening: http://0.0.0.0:3000
MCP server: http://0.0.0.0:3001/mcp
2. Connect an MCP Client
The MCP server exposes a JSON-RPC endpoint. You can call it directly from any HTTP client or custom agent:
# List available tools (MCP-audience JWT; opaque HONE_MCP_KEYS and HONE_API_KEYS still work)
curl http://pi-hostname:3001/mcp -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $HONE_MCP_KEY" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# Call a tool
curl http://pi-hostname:3001/mcp -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $HONE_MCP_KEY" \
-d '{
"jsonrpc":"2.0",
"id":2,
"method":"tools/call",
"params":{
"name":"get_spending_summary",
"arguments":{"period":"this-month"}
}
}'
When auth is required (the default), unauthenticated requests to /mcp receive 401. --no-auth is accepted only on a loopback bind (127.0.0.1, ::1, or localhost).
Available Tools
| Tool | Description | Key Parameters |
|---|---|---|
search_transactions |
Find transactions | query, tag, period, min_amount, max_amount |
get_spending_summary |
Spending by category | period |
get_subscriptions |
List subscriptions | status (active/cancelled/excluded/all) |
get_alerts |
Waste detection alerts | alert_type, include_dismissed |
compare_spending |
Period comparison | current_period, baseline_period |
get_merchants |
Top merchants | period, category, limit |
get_account_summary |
Account overview | — |
Period Presets
All period parameters accept:
this-month,last-monththis-year,last-year,ytdlast-30-days,last-90-days,last-12-monthsall- Custom date:
2024-01-15
Example Conversations
Once connected, you can ask questions naturally:
Basic Queries
"What did I spend on groceries last month?"
The LLM calls get_spending_summary with period: "last-month" and filters for the Groceries category.
"Show me my Amazon purchases this year"
Calls search_transactions with query: "Amazon" and period: "this-year".
Subscription Management
"What subscriptions am I paying for?"
Calls get_subscriptions with status: "active".
"Do I have any zombie subscriptions?"
Calls get_alerts with alert_type: "zombie".
Spending Analysis
"How does my spending this month compare to last month?"
Calls compare_spending with default periods.
"Where am I spending the most money?"
Calls get_merchants with period: "this-year".
Follow-up Questions
The LLM maintains context, so you can ask follow-ups:
"What about just dining?"
If the previous query was about spending, it calls the same tool filtered to "Dining".
"Show me those transactions"
Calls search_transactions to drill into details.
Homelab Setup Example
Here's a complete homelab configuration:
Network Setup
Router (192.168.1.1)
├── Raspberry Pi 4 (192.168.1.50)
│ └── Hone: ports 3000 (API) + 3001 (MCP)
└── Mac Studio (192.168.1.100)
└── Ollama: port 11434
Pi Configuration
/etc/systemd/system/hone.service:
[Unit]
Description=Hone Personal Finance
After=network.target
[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/hone
Environment=HONE_DB_KEY=your-encryption-key
Environment=HONE_API_KEYS=your-generated-api-key
Environment=HONE_MCP_RESOURCE=http://192.168.1.50:3001/mcp
Environment=HONE_MCP_JWT_SECRET=your-signing-secret
Environment=OLLAMA_HOST=http://192.168.1.100:11434
Environment=OLLAMA_MODEL=llama3.2
ExecStart=/home/pi/hone/hone serve \
--port 3000 \
--mcp-port 3001 \
--host 0.0.0.0 \
--static-dir /home/pi/hone/ui/dist
Restart=always
[Install]
WantedBy=multi-user.target
sudo systemctl enable hone
sudo systemctl start hone
Once running, query the MCP endpoint from any HTTP client on your network:
curl http://192.168.1.50:3001/mcp -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-generated-mcp-key" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_spending_summary","arguments":{"period":"this-month"}}}'
Security Considerations
Authentication
MCP uses the same auth_middleware as /api. When auth is required (the default), /mcp is not reachable without credentials.
Hone's MCP port is an OAuth 2.1 resource server (MCP Authorization 2026-07-28), not an authorization server. The MCP resource is distinct from the main API (http://host:3001/mcp vs http://host:3000/api).
Implemented:
- RFC 9728 Protected Resource Metadata at
/.well-known/oauth-protected-resourceand/.well-known/oauth-protected-resource/mcp(public, on the MCP port) - RFC 6750
WWW-Authenticateon/mcp401s, withresource_metadataandscope="mcp:read" - RFC 8707 audience-bound JWTs:
audmust beHONE_MCP_RESOURCE. Mint locally withhone mcp-token(HONE_MCP_JWT_SECRET, HS256) or validate RS256 tokens from an external AS (HONE_MCP_JWKS_URL, optionalHONE_MCP_ISSUER/HONE_MCP_AUTHORIZATION_SERVERS) - MCP-audience JWTs are rejected on
/api
Skipped (too large for this chip; not theater):
- Hosting an AS (RFC 8414 metadata,
/authorize,/token, PKCE, Dynamic Client Registration, Client ID Metadata Documents) - Refresh tokens /
offline_access - DPoP
- Step-up
403 insufficient_scope(all tools sharemcp:read)
Opaque fallbacks (not MCP OAuth tokens):
HONE_MCP_KEYS— accepted on/mcp, rejected on/apiHONE_API_KEYS— accepted on/apiand/mcp
Cloudflare Access JWT / user header and HONE_TRUSTED_NETWORKS still authenticate both ports. Those are session/network identity, not resource-bound tokens.
--no-auth leaves both the API and MCP open, consistent with the REST API. serve refuses that flag unless --host is loopback.
--mcp-allowed-hosts is a Host-header allowlist for DNS-rebinding protection. It is not authentication: listing a hostname does not grant access.
CsrfLayer wraps /api only. MCP tools are read-only and clients are non-browser (Bearer or trusted-net), so they are not subject to that check. See Deployment — CSRF Protection.
Network Isolation
The MCP server binds to the same --host as the API:
--host 127.0.0.1— Only local connections (default)--host 0.0.0.0— All network interfaces (for LAN access)
Do NOT expose --mcp-port to the internet. On a private LAN, prefer MCP-audience JWTs (HONE_MCP_JWT_SECRET + hone mcp-token, or an external AS). Opaque HONE_MCP_KEYS / HONE_API_KEYS and trusted networks still work. --no-auth is not accepted on a published bind.
Firewall
If you have a firewall on the Pi:
# Allow MCP port from local network only
sudo ufw allow from 192.168.1.0/24 to any port 3001
Troubleshooting
"Connection refused"
- Check Hone is running:
systemctl status hone - Check it's listening on all interfaces:
ss -tlnp | grep 3001 - Check firewall:
sudo ufw status
"No tools available" in Claude Desktop
- Check the MCP endpoint is reachable:
curl -H "Authorization: Bearer $HONE_MCP_KEY" http://pi:3001/mcp - Verify config file syntax (JSON must be valid)
- Restart Claude Desktop completely
Tools return empty results
- Make sure you have data imported
- Check the time period — default is "this-month"
- Try
period: "all"to see all data
Slow responses
MCP tools query the database directly — they should be fast (<100ms). If slow:
- Check Pi CPU/memory:
htop - Check database size:
ls -lh hone.db - Consider adding indexes (rare)
Advanced: Building a Custom Agent
If you want to build your own conversational agent using Ollama + Hone MCP:
import httpx
import json
MCP_URL = "http://192.168.1.50:3001/mcp"
MCP_HEADERS = {"Authorization": "Bearer your-generated-mcp-key"}
def call_tool(name: str, args: dict) -> dict:
"""Call an MCP tool and return the result."""
response = httpx.post(MCP_URL, headers=MCP_HEADERS, json={
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": name,
"arguments": args
}
})
return response.json()
# Example: Get spending summary
result = call_tool("get_spending_summary", {"period": "this-month"})
print(json.dumps(result, indent=2))
Integrate this with your Ollama agent's tool-calling capability for a fully local conversational finance assistant.
What's Next
The MCP server provides read-only access. Future enhancements could include:
- Write tools (mark subscription as cancelled, dismiss alert)
- Streaming for large result sets
- WebSocket transport for real-time updates