Skip to main content
Glama
sttts

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'
```