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

VariableRequiredDescription
TRAILHUB_API_KEYyesYour API key (th_…)
TRAILHUB_API_URLnoAPI origin; defaults to https://trailhub.org

#Setup

#Claude Desktop

  1. Open Claude Desktop → Settings → Developer → Edit Config. This opens claude_desktop_config.json.

  2. Add a trailhub entry under mcpServers. The simplest form runs the server straight from GitHub with npx:

    {
      "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-hub repository (run npm install inside mcp-server first):

    {
      "mcpServers": {
        "trailhub": {
          "command": "node",
          "args": ["/path/to/trail-hub/mcp-server/index.js"],
          "env": { "TRAILHUB_API_KEY": "th_..." }
        }
      }
    }
  3. Save and restart Claude Desktop. A tools icon should show trailhub with its tools.

#Claude Code

claude mcp add trailhub -e TRAILHUB_API_KEY=th_... -- node /path/to/trail-hub/mcp-server/index.js

Or 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

ToolWhat it doesArguments
whoamiShow which manager account and key the server is using, including scopes and any trail-system restrictionnone

#Trail systems

ToolWhat it doesArguments
list_trail_systemsList the systems the key can access with ids, roles and open/caution/closed counts. Assistants call this first to find an idnone
get_trail_systemFull details: description, contact info, units, language, stats, active updates, latest weathertrailSystemId
update_trail_systemEdit public details (owner/administrator only); only passed fields changetrailSystemId, name?, description?, phoneNumber?, website?, language? (en/fr), tempMeasurement? (fahrenheit/celsius), depthMeasurement?, lengthMeasurement?, inactive?, hideName?
get_weatherLatest hourly weather snapshot (paid tiers; may be null)trailSystemId
recompile_trail_systemForce a rebuild of the public GeoJSON and stats; normally unnecessarytrailSystemId

#Trails

ToolWhat it doesArguments
list_trailsAll trails with id, name, status, difficulty, distance (km), type, hidden flag and per-activity statuses (no geometry)trailSystemId
get_trailOne trail including GeoJSON geometrytrailId
update_trailChange 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 activitiestrailId, 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_statusesOpen, close or mark caution on many trails, or all trails, in one call and one recompiletrailSystemId, status, trailIds? or allTrails?
create_trailCreate a trail from a GeoJSON LineString/MultiLineString ([lng, lat] pairs); distance is computed. Activities must be added in the web app afterwardstrailSystemId, name, geometry, status?, difficulty?, description?, type?, hidden?
delete_trailMove a trail to the trash. The tool description instructs the assistant to confirm with you firsttrailId

#Points of interest

ToolWhat it doesArguments
list_pointsPoints (parking, trailheads, lodges, hazards, webcams, …) with status and coordinatestrailSystemId
create_pointAdd a point at a lat/lngtrailSystemId, name, markerClass, lat, lng, description?, status?, hidden?, waitTime?, webCamUrl?
update_pointChange status, name, description, marker, location, wait time or webcam URLpointId, plus any of the fields above
delete_pointMove a point to the trash. Confirms with you firstpointId

#Updates (notices, surface conditions, snow reports)

ToolWhat it doesArguments
list_updatesActive updates; includeExpired for historytrailSystemId, includeExpired?
post_noticePublish a general notice. notify: true emails/texts subscribers, so the assistant is told to confirm wording firsttrailSystemId, description, severity (info default, warning, danger), linkUrl?, expiresInHours? (default 24), expirationDate?, trailIds?, notify?
post_surface_conditionsReport surface conditions for specific trails or all trailstrailSystemId, conditions[], trailIds? or allTrails?, expiresInHours?, expirationDate?, notify?
post_snow_reportPublish snow totals in the system's depth unit; omit unknown fieldstrailSystemId, baseDepth?, twentyFourHours?, fourtyEightHours?, sevenDays?, seasonTotal?, lastSnowAmount?, lastSnowDate?, lastSnowTime? ("HH:MM"), upperElevationDepth?, lastSnowMakingDate?, expiresInHours?, expirationDate?, notify?
update_updateEdit an existing update: text/fields, expiry, trailsupdateId, plus description?, severity?, linkUrl?, conditions?, trailIds?, allTrails?, expiresInHours?, expirationDate?, notify?, snow? (object of snow fields)
delete_updateRemove an update immediatelyupdateId

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_trail and delete_point tell 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_update is immediate.
  • notify: true sends 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

SymptomCauseFix
Server exits at startup with TRAILHUB_API_KEY is not setThe env block is missing or misspelledAdd TRAILHUB_API_KEY to the server's env in your client config and restart the client
Tool results say 401: invalid_api_keyKey mistyped or truncatedCopy the full key (it starts with th_ and is 43 characters)
401: revoked_api_key or 401: expired_api_keyKey was revoked or has passed its expiryCreate a new key in User Settings → API Keys and update the config
403: insufficient_scopeThe key is read-only but the assistant tried to writeCreate a read & write key, or tell the assistant it can only read
403: key_restrictedThe key is limited to one trail system and the assistant targeted anotherUse a key without a restriction, or ask about the permitted system
403: forbiddenYour account does not manage that trail systemAsk the system's owner to add you as a trustee
403: admin_requiredupdate_trail_system needs the Administrator roleAsk the owner to make the change or grant you Administrator
404: trail_not_found / point_not_found / update_not_foundId is wrong or belongs to another systemHave the assistant re-list and retry
Tools do not appear in Claude DesktopConfig JSON invalid, or client not restartedValidate the JSON and fully quit and reopen Claude Desktop
npx form is slow to startIt downloads the package on first runNormal; 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 test

createServer() is exported so the same tool set can later be hosted remotely (Streamable HTTP).