Manage your trail system by talking to Claude

It's 6am. The thermometer on the shop wall says 19°F, there's a glaze of ice on everything, and you already know three trails are not opening today. Your phone is in one gloved hand, the radio is in the other, and the thing you need to do before anyone shows up at the trailhead is update the public map, post a warning, and tell your subscribers.

Tapping through a web app in a cold glove is fine. It is just not what you want to be doing right then. What you want is to say, in one line, "Close Ridge Run, Beaver Pond and the Connector. Post a danger notice about ice and notify subscribers." And have it happen.

That is what the TrailHUB MCP server does. It connects Claude (Claude Desktop, Claude Code, or any MCP client) to your TrailHUB account through the Management API. Every action runs as you, with exactly the permissions you give it. When Claude closes a trail, it writes the same document the web app would, and the public map, stats and embeds rebuild within a few seconds.

Here is the setup in three steps, then a realistic morning.

#What you'll need

  • A TrailHUB manager account with at least one trail system.
  • Claude Desktop (or Claude Code) on your computer.
  • Node 18 or newer. The MCP server is a small Node program that runs locally on your machine and talks to TrailHUB over HTTPS.

One honest note up front: today the MCP server runs locally over stdio. Claude Desktop launches it on your laptop and talks to it directly. There is no hosted version yet, so a plain claude.ai browser tab cannot connect on its own. More on that at the end.

#Step 1: Create an API key

  1. Sign in to TrailHUB and open User Settings → API Keys.
  2. Give the key a name you'll recognize later, such as "Claude assistant".
  3. Under Access, choose Read & write. (If you only want Claude to answer questions, pick Read only. See the tips below.)
  4. Under Limit to one trail system, optionally pick a single system. By default the key can reach every trail system you manage; restricting it is a good idea if you manage several and only want the assistant touching one.
  5. Choose an expiry if you like (30 days, 90 days, a year, or never).
  6. Click Create API key and copy the key right away. It starts with th_ and is shown exactly once. TrailHUB stores only a hash of it, so if you lose it you'll need to create a new one.

Treat it like a password. The key does whatever your account can do, within the scope and trail-system restriction you set.

#Step 2: Add the server to Claude Desktop

Open Claude Desktop's claude_desktop_config.json (Settings → Developer → Edit Config gets you there) and add a trailhub entry under mcpServers, pasting your key into TRAILHUB_API_KEY:

{
  "mcpServers": {
    "trailhub": {
      "command": "npx",
      "args": ["-y", "github:Orbitist/trail-hub#main:mcp-server"],
      "env": { "TRAILHUB_API_KEY": "th_..." }
    }
  }
}

This uses npx to fetch and run the server straight from the TrailHUB repository on GitHub. If you'd rather run from a local checkout of the repo, point at the file instead:

{
  "mcpServers": {
    "trailhub": {
      "command": "node",
      "args": ["/path/to/trail-hub/mcp-server/index.js"],
      "env": { "TRAILHUB_API_KEY": "th_..." }
    }
  }
}

The only required environment variable is TRAILHUB_API_KEY. An optional TRAILHUB_API_URL lets you point at a different API origin; it defaults to https://trailhub.org.

#Using Claude Code instead

If you live in the terminal, Claude Code can use the same server. From a local checkout:

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

Everything below works the same way there.

#Step 3: Restart Claude and say hello

Quit and reopen Claude Desktop so it picks up the new config. You should see the trailhub tools listed in the tools menu of a new conversation.

Try an easy question first:

You: Which of my trail systems are open?

Behind the scenes Claude calls list_trail_systems, which returns each system you manage with its id, your role, and open/closed trail counts. You'll get back something like:

Claude: You manage two trail systems. Chautauqua Rails has 14 of 16 trails open (2 closed). Lakeside Nordic has all 11 trails open.

If you ever want to check what the server is acting as, ask "who am I?" and Claude will call whoami, which shows the account, the key's scopes, and any trail-system restriction.

#A morning after an ice storm

Here is the scenario from the top of this post, played out. Assume the key is read & write and limited to Chautauqua Rails.

You: We had freezing rain overnight. Close Ridge Run, Beaver Pond Trail and the Connector.

Claude calls list_trails for Chautauqua Rails to match the names to trail ids, then calls set_trail_statuses once with those three ids and status: "Closed". One call, one recompile. Setting a trail to Closed also closes every activity on it, exactly as it does in the web app.

Claude: Done. Ridge Run, Beaver Pond Trail and Connector are now Closed. Chautauqua Rails is showing 11 open, 5 closed.

Within a few seconds the public map redraws those three trails as closed, the trail list updates, and any embed on your website follows.

You: Post a danger notice: "Freezing rain overnight. Ridge Run, Beaver Pond and the Connector are closed until crews can treat them. Use extreme caution on all other trails." Expires at noon. Notify subscribers.

Because notify: true sends real email and SMS to members of the public who subscribed to your system, Claude is instructed to confirm the wording first:

Claude: I'm about to post this as a danger notice, related to the three closed trails, expiring at 12:00 today, and send it by email and SMS to your subscribers:

"Freezing rain overnight. Ridge Run, Beaver Pond and the Connector are closed until crews can treat them. Use extreme caution on all other trails."

Go ahead?

You: Yes.

Claude calls post_notice with severity: "danger", the three trailIds, an expirationDate of noon, and notify: true. The notice appears on the public page and map immediately, and subscribers get the same message they would have received had you posted it from the web app.

Two hours later, the groomer is done on the lower trails.

You: Mark Lakeside Loop and Meadow Trail as groomed and track-set.

Claude calls post_surface_conditions with conditions: ["groomed", "track-set"] and the two trail ids. Surface conditions show on each trail for 24 hours by default. If you groom again tomorrow, just say so and a new report replaces the picture.

You: Snow report: 4 inches in the last 24 hours, 6 in 48, base depth 14.

Claude calls post_snow_report with twentyFourHours: 4, fourtyEightHours: 6 and baseDepth: 14. Amounts are in whatever depth unit your trail system uses (inches here). Anything you don't mention is simply left out.

Claude: Posted. The snow report now shows 4" in 24h, 6" in 48h, and a 14" base.

By 8:15 the public map is telling the truth: three trails closed with a red danger banner, two freshly track-set, today's snow totals up top. You did it between radio calls without taking off a glove.

When the crews have sanded, "Reopen the three closed trails and take down the ice notice" is set_trail_statuses with status: "Open", then list_updates and delete_update. To soften the notice instead of removing it, Claude uses update_update.

#Everything the server can do

The full tool list, for reference:

AreaTools
Accountwhoami
Trail systemslist_trail_systems, get_trail_system, update_trail_system, get_weather
Trailslist_trails, get_trail, update_trail, set_trail_statuses, create_trail, delete_trail
Points of interestlist_points, create_point, update_point, delete_point
Updateslist_updates, post_notice, post_surface_conditions, post_snow_report, update_update, delete_update
Maintenancerecompile_trail_system

You never have to name these. You talk; Claude picks the tool. "Add a parking point at 42.41, -79.31 called 'Lot A'", "Set the Lot B wait time point to Closed", or "Which notices are active and when do they expire?" all work. Creating trails from GeoJSON geometry works too, though activities still have to be added to a new trail in the web app.

#Tips

Use a read-only key for assistants that only answer questions. If front-desk staff should be able to ask "is the Connector open?" with no chance of changing anything, create a key with Read only access. Every write tool fails with a clear permission error.

Confirm before notify and delete. The server's tool descriptions ask Claude to confirm before sending notifications or deleting anything, and in our experience it does. It still doesn't hurt to say "ask me before you notify anyone" at the start of a conversation. Deleted trails and points go to the trash and can be recovered in the web app; deleted updates are gone, same as if they'd expired.

Restrict the key to one trail system when you can. A key limited to one system cannot touch the others, no matter what gets typed.

You can revoke a key any time. Back in User Settings → API Keys, click Revoke. Anything using that key stops working immediately. The same tab shows when each key was last used.

Keep the key out of chat. The key belongs in the config file's env block, not in the conversation. Claude never needs to see it.

#What's next

Today the server runs locally over stdio, which is simple and private but means a laptop has to be involved. Two things are on our list:

  • A hosted MCP endpoint, so claude.ai (including the mobile apps) can connect to TrailHUB with no local install. The server is already written to be embedded in a remote host; what's left is hosting and sign-in.
  • Remote agents on a schedule, so a Claude task can run every morning, check conditions, and draft (or, with your approval, post) the day's report.

If you try it this season, tell us what you ask it to do, especially the things it gets wrong. Docs: Management API and MCP server.