MCP server
Connect Claude Desktop, Claude Code or any MCP client to TrailHUB so you can manage trails, notices and conditions in plain language.
The TrailHUB MCP server lets an AI assistant read and manage your trail systems in natural language: "Which of my systems have closed trails?", "Close everything at Ridge Park and post a high-wind notice", "Mark Lakeside Loop groomed and track-set."
#What MCP is
The Model Context Protocol (MCP) is an open standard that lets AI applications call external tools. An MCP server is a small program that describes a set of tools (here: "list trails", "post a notice", …) and runs them when the assistant asks. The TrailHUB MCP server is a thin wrapper over the Management API v1: every tool call becomes an API request made with your API key, so it can do exactly what that key allows and nothing more.
#Prerequisites
- Node.js 18 or newer on the computer that runs your MCP client.
- A TrailHUB API key from User Settings → API Keys (see API keys). Name it after the assistant, e.g. "Claude assistant". Use a read-only key if the assistant only needs to answer questions.
- An MCP client: Claude Desktop, Claude Code, Cursor, or any client that supports stdio servers.
The server runs over stdio (no network port) and reads its configuration from environment variables.
#Environment variables
| Variable | Required | Description |
|---|---|---|
TRAILHUB_API_KEY | yes | Your API key (th_…) |
TRAILHUB_API_URL | no | API origin; defaults to https://trailhub.org |
#Setup
#Claude Desktop
Open Claude Desktop → Settings → Developer → Edit Config. This opens
claude_desktop_config.json.Add a
trailhubentry undermcpServers. The simplest form runs the server straight from GitHub withnpx:{ "mcpServers": { "trailhub": { "command": "npx", "args": ["-y", "github:Orbitist/trail-hub#main:mcp-server"], "env": { "TRAILHUB_API_KEY": "th_..." } } } }Or, from a local checkout of the
trail-hubrepository (runnpm installinsidemcp-serverfirst):{ "mcpServers": { "trailhub": { "command": "node", "args": ["/path/to/trail-hub/mcp-server/index.js"], "env": { "TRAILHUB_API_KEY": "th_..." } } } }Save and restart Claude Desktop. A tools icon should show
trailhubwith its tools.
#Claude Code
claude mcp add trailhub -e TRAILHUB_API_KEY=th_... -- node /path/to/trail-hub/mcp-server/index.jsOr with npx:
claude mcp add trailhub -e TRAILHUB_API_KEY=th_... -- npx -y "github:Orbitist/trail-hub#main:mcp-server"Run claude mcp list to confirm it is registered.
#Other stdio clients (Cursor, etc.)
Any client that launches a stdio MCP server needs three things: the command (node or npx), its arguments (the path to index.js, or the github:… package spec), and the TRAILHUB_API_KEY environment variable. Consult your client's documentation for where to put them; the shape is the same as the Claude Desktop JSON above.
#Tools
Every tool returns the API's JSON response as text. Write tools trigger a recompile, so the public map, stats and embeds update within seconds.
#Account
| Tool | What it does | Arguments |
|---|---|---|
whoami | Show which manager account and key the server is using, including scopes and any trail-system restriction | none |
#Trail systems
| Tool | What it does | Arguments |
|---|---|---|
list_trail_systems | List the systems the key can access with ids, roles and open/caution/closed counts. Assistants call this first to find an id | none |
get_trail_system | Full details: description, contact info, units, language, stats, active updates, latest weather | trailSystemId |
update_trail_system | Edit public details (owner/administrator only); only passed fields change | trailSystemId, name?, description?, phoneNumber?, website?, language? (en/fr), tempMeasurement? (fahrenheit/celsius), depthMeasurement?, lengthMeasurement?, inactive?, hideName? |
get_weather | Latest hourly weather snapshot (paid tiers; may be null) | trailSystemId |
recompile_trail_system | Force a rebuild of the public GeoJSON and stats; normally unnecessary | trailSystemId |
#Trails
| Tool | What it does | Arguments |
|---|---|---|
list_trails | All trails with id, name, status, difficulty, distance (km), type, hidden flag and per-activity statuses (no geometry) | trailSystemId |
get_trail | One trail including GeoJSON geometry | trailId |
update_trail | Change status, name, description, difficulty, visibility, type, line style, one-way flags, order, categories or per-activity statuses. Closed closes every activity; None derives status from activities | trailId, plus any of status, name, description, difficulty, hidden, type, lineStyle, oneWay, oneWayReversed, order, categories, activities[] ({ value, status?, difficulty? }, activity must already be on the trail) |
set_trail_statuses | Open, close or mark caution on many trails, or all trails, in one call and one recompile | trailSystemId, status, trailIds? or allTrails? |
create_trail | Create a trail from a GeoJSON LineString/MultiLineString ([lng, lat] pairs); distance is computed. Activities must be added in the web app afterwards | trailSystemId, name, geometry, status?, difficulty?, description?, type?, hidden? |
delete_trail | Move a trail to the trash. The tool description instructs the assistant to confirm with you first | trailId |
#Points of interest
| Tool | What it does | Arguments |
|---|---|---|
list_points | Points (parking, trailheads, lodges, hazards, webcams, …) with status and coordinates | trailSystemId |
create_point | Add a point at a lat/lng | trailSystemId, name, markerClass, lat, lng, description?, status?, hidden?, waitTime?, webCamUrl? |
update_point | Change status, name, description, marker, location, wait time or webcam URL | pointId, plus any of the fields above |
delete_point | Move a point to the trash. Confirms with you first | pointId |
#Updates (notices, surface conditions, snow reports)
| Tool | What it does | Arguments |
|---|---|---|
list_updates | Active updates; includeExpired for history | trailSystemId, includeExpired? |
post_notice | Publish a general notice. notify: true emails/texts subscribers, so the assistant is told to confirm wording first | trailSystemId, description, severity (info default, warning, danger), linkUrl?, expiresInHours? (default 24), expirationDate?, trailIds?, notify? |
post_surface_conditions | Report surface conditions for specific trails or all trails | trailSystemId, conditions[], trailIds? or allTrails?, expiresInHours?, expirationDate?, notify? |
post_snow_report | Publish snow totals in the system's depth unit; omit unknown fields | trailSystemId, baseDepth?, twentyFourHours?, fourtyEightHours?, sevenDays?, seasonTotal?, lastSnowAmount?, lastSnowDate?, lastSnowTime? ("HH:MM"), upperElevationDepth?, lastSnowMakingDate?, expiresInHours?, expirationDate?, notify? |
update_update | Edit an existing update: text/fields, expiry, trails | updateId, plus description?, severity?, linkUrl?, conditions?, trailIds?, allTrails?, expiresInHours?, expirationDate?, notify?, snow? (object of snow fields) |
delete_update | Remove an update immediately | updateId |
Enumerations (statuses, difficulties, marker classes, surface conditions) are the same as in the Management API reference.
#Example prompts
Once connected, try:
- "Which of my trail systems have closed trails right now?"
- "List the trails at Chautauqua Rails with their status."
- "Close every trail at Chautauqua Rails and post a danger notice that we're closed for high winds until tomorrow morning. Notify subscribers."
- "Mark Lakeside Loop and Ridge Run as groomed and track-set."
- "Post a snow report: 6 inches in the last 24 hours, base depth 18."
- "Add a parking point at 42.41, -79.31 called 'Lot A'."
- "Set the parking lot wait-time point to Closed."
- "Extend this morning's grooming report so it expires at 6 pm."
- "Reopen everything we closed yesterday."
The assistant will usually call list_trail_systems and list_trails first to resolve names into ids.
#Safety notes
- Everything happens as you. Actions are recorded with your account as the author, exactly as if you had done them in the web app.
- Deletes ask first.
delete_trailanddelete_pointtell the assistant to confirm with you before calling. Deleted trails and points go to the trash and can be restored by a manager in the web app;delete_updateis immediate. notify: truesends real email and SMS to everyone subscribed to the trail system, the same as ticking "Notify" in the web app. Ask the assistant to show you the wording before it posts with notify, or tell it never to notify.- Use a read-only key for assistants that only need to read. They can still answer "what's open?" but cannot change anything.
- Restrict the key to one trail system if the assistant should only work on one.
- Revoke the key in User Settings → API Keys if a laptop is lost or a config file is shared by mistake; the server stops working immediately.
- The server has no memory of its own and stores nothing locally; all state lives in TrailHUB.
#Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Server exits at startup with TRAILHUB_API_KEY is not set | The env block is missing or misspelled | Add TRAILHUB_API_KEY to the server's env in your client config and restart the client |
Tool results say 401: invalid_api_key | Key mistyped or truncated | Copy the full key (it starts with th_ and is 43 characters) |
401: revoked_api_key or 401: expired_api_key | Key was revoked or has passed its expiry | Create a new key in User Settings → API Keys and update the config |
403: insufficient_scope | The key is read-only but the assistant tried to write | Create a read & write key, or tell the assistant it can only read |
403: key_restricted | The key is limited to one trail system and the assistant targeted another | Use a key without a restriction, or ask about the permitted system |
403: forbidden | Your account does not manage that trail system | Ask the system's owner to add you as a trustee |
403: admin_required | update_trail_system needs the Administrator role | Ask the owner to make the change or grant you Administrator |
404: trail_not_found / point_not_found / update_not_found | Id is wrong or belongs to another system | Have the assistant re-list and retry |
| Tools do not appear in Claude Desktop | Config JSON invalid, or client not restarted | Validate the JSON and fully quit and reopen Claude Desktop |
npx form is slow to start | It downloads the package on first run | Normal; or install from a local checkout and use the node form |
To see exactly what the key can do, ask the assistant to run whoami, or call GET /api/v1/me yourself with curl.
#Developing the server
The server is a single ES module at mcp-server/index.js in the trail-hub repository.
cd mcp-server
npm install
npm testcreateServer() is exported so the same tool set can later be hosted remotely (Streamable HTTP).