caltopo-mcp
by sttts
README.md
# caltopo-mcp
HTTP MCP server for managing CalTopo maps from headless agents.
This server is designed for a Hermes/Codex-style agent that needs to list maps, create folders, manage markers, and import/export GPX/KML without using a browser session.
## Status
- Uses CalTopo private-account activation credentials.
- Does not require Chrome, Playwright, a desktop login, or a UI session.
- Runs as a normal headless Node.js process.
- Exposes a local HTTP MCP endpoint.
- Tested against a private CalTopo account for read/write marker and folder operations.
CalTopo does not publicly document this private-account API for general third-party use. Treat the integration as pragmatic and test bulk writes on disposable maps first.
## Repository
Canonical repository:
```text
https://github.com/sttts/caltopo-mcp
```
Do not commit local credentials. Runtime secrets belong in `.data/`, environment variables, or a local secret manager.
## Features
- Account auth check
- Map listing
- Raw map data retrieval
- Folder list/create/update/delete
- Marker list/create/update/delete
- GPX waypoint import as CalTopo markers
- KML point placemark import as CalTopo markers
- GPX/KML marker export
- Optional Bearer token on the MCP endpoint
- macOS LaunchDaemon template for headless operation
## Requirements
- Node.js 22 or newer
- npm
- A CalTopo account
- CalTopo activation credentials
On the Hermes Mac mini, the bundled Node runtime is expected at:
```text
/Users/hermes/.hermes/node/bin/node
```
## Credentials
The server supports two credential modes.
### Activation File
Preferred for local operation:
```text
CALTOPO_ACTIVATION_FILE=.data/activation.local.json
```
The activation file has this shape:
```json
{
"account": {
"id": "..."
},
"code": "...",
"key": "..."
}
```
Keep the file outside Git:
```sh
mkdir -p .data
chmod 700 .data
chmod 600 .data/activation.local.json
```
`.data/` is ignored by Git.
### Environment Variables
Alternative for service managers or secret stores:
```text
CALTOPO_ACCOUNT_ID=...
CALTOPO_CREDENTIAL_ID=...
CALTOPO_CREDENTIAL_KEY=...
```
Never hardcode these values in source files, plist files, shell scripts, README examples, or committed config.
## Getting Activation Credentials
Open an activation URL in a browser where you are logged in to CalTopo:
```text
https://caltopo.com/app/activate/localhost/client/finish-activate?code=<CODE>&name=mcp
```
Then resolve the code locally:
```sh
CALTOPO_ACTIVATION_CODE=<CODE> python3 - <<'PY'
import os
import urllib.request
code = os.environ["CALTOPO_ACTIVATION_CODE"]
path = ".data/activation.local.json"
os.makedirs(".data", exist_ok=True)
with urllib.request.urlopen(f"https://caltopo.com/api/v1/activate?code={code}", timeout=20) as response:
data = response.read()
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, "wb") as handle:
handle.write(data)
print(f"wrote {path}")
PY
```
## Local Development
```sh
npm install
npm run typecheck
npm run build
CALTOPO_ACTIVATION_FILE=.data/activation.local.json npm start
```
Default endpoint:
```text
http://127.0.0.1:8788/mcp
```
Health check:
```sh
curl -sS http://127.0.0.1:8788/health
```
The health endpoint intentionally does not return credential values or account IDs.
## Configuration
Environment variables:
```text
CALTOPO_ACTIVATION_FILE=.data/activation.local.json
CALTOPO_ACCOUNT_ID=...
CALTOPO_CREDENTIAL_ID=...
CALTOPO_CREDENTIAL_KEY=...
CALTOPO_DOMAIN=caltopo.com
CALTOPO_MCP_HOST=127.0.0.1
CALTOPO_MCP_PORT=8788
CALTOPO_MCP_TOKEN=optional-bearer-token
```
`CALTOPO_MCP_TOKEN` protects the MCP endpoint with:
```text
Authorization: Bearer <token>
```
Keep the server bound to `127.0.0.1` unless you also configure authentication and network-level access control.
## MCP Tools
- `caltopo_auth_status`
- `caltopo_list_maps`
- `caltopo_get_map`
- `caltopo_list_folders`
- `caltopo_create_folder`
- `caltopo_update_folder`
- `caltopo_delete_folder`
- `caltopo_list_markers`
- `caltopo_create_marker`
- `caltopo_update_marker`
- `caltopo_delete_marker`
- `caltopo_import_markers`
- `caltopo_export_markers`
## Example Tool Payloads
Create a marker:
```json
{
"map_id": "ABCDE12",
"title": "Camp",
"lat": 65.90993,
"lon": -23.84598,
"description": "Overnight stop",
"color": "#00AAFF",
"symbol": "point"
}
```
Import GPX waypoints:
```json
{
"map_id": "ABCDE12",
"format": "gpx",
"content": "<?xml version=\"1.0\"?><gpx><wpt lat=\"65.9\" lon=\"-23.8\"><name>Test</name></wpt></gpx>",
"dry_run": true
}
```
Use `dry_run: true` before bulk imports.
## Headless Service Scripts
Manual operation:
```sh
bin/start-caltopo-mcp.sh
bin/status-caltopo-mcp.sh
bin/stop-caltopo-mcp.sh
```
Do not use cron for this service. On macOS, use the LaunchDaemon in `deploy/local.caltopo-mcp.plist` if the server must survive reboots without an active UI login.
## Hermes Deployment
Recommended layout on the Mac mini:
```text
/Users/hermes/Quellen/caltopo-mcp
/Users/hermes/Quellen/caltopo-mcp/.data/activation.local.json
```
Clone and build:
```sh
cd /Users/hermes/Quellen
git clone https://github.com/sttts/caltopo-mcp.git caltopo-mcp
cd caltopo-mcp
PATH=/Users/hermes/.hermes/node/bin:$PATH npm install
PATH=/Users/hermes/.hermes/node/bin:$PATH npm run build
```
Start manually:
```sh
cd /Users/hermes/Quellen/caltopo-mcp
bin/start-caltopo-mcp.sh
```
Register with Hermes:
```sh
/Users/hermes/.local/bin/hermes mcp add caltopo --url http://127.0.0.1:8788/mcp
/Users/hermes/.local/bin/hermes mcp test caltopo
```
## macOS LaunchDaemon
For reboot-safe headless operation, install the LaunchDaemon as root:
```sh
cd /Users/hermes/Quellen/caltopo-mcp
sudo bin/install-launchdaemon.sh
```
The plist runs the process as `hermes`:
```text
/Library/LaunchDaemons/local.caltopo-mcp.plist
```
Check the service:
```sh
launchctl print system/local.caltopo-mcp
bin/status-caltopo-mcp.sh
/Users/hermes/.local/bin/hermes mcp test caltopo
```
Remove it:
```sh
sudo launchctl bootout system /Library/LaunchDaemons/local.caltopo-mcp.plist
sudo rm -f /Library/LaunchDaemons/local.caltopo-mcp.plist
```
## Updating
```sh
cd /Users/hermes/Quellen/caltopo-mcp
git pull --ff-only
PATH=/Users/hermes/.hermes/node/bin:$PATH npm install
PATH=/Users/hermes/.hermes/node/bin:$PATH npm run build
bin/stop-caltopo-mcp.sh
bin/start-caltopo-mcp.sh
```
If the LaunchDaemon is installed:
```sh
sudo launchctl kickstart -k system/local.caltopo-mcp
```
## Security Checklist
- `.data/` is ignored by Git.
- Activation credentials are not present in source files.
- The health endpoint does not expose account IDs or credential metadata.
- The default bind address is `127.0.0.1`.
- Use `CALTOPO_MCP_TOKEN` before exposing the endpoint beyond loopback.
- Prefer `dry_run` for imports before writing many markers.
Before publishing:
```sh
git status --short
git ls-files
rg -n "activation\\.local|CALTOPO_CREDENTIAL|credentialKey|Bearer|token|gmail|account_id" . \
-g '!node_modules' -g '!dist' -g '!.data' -g '!.git'
```
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues