Skip to main content
Glama
mcarch-drdoof

griptonite-mcp

README.md
# griptonite-mcp

[![CI](https://github.com/mcarch-drdoof/griptonite_mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/mcarch-drdoof/griptonite_mcp/actions/workflows/ci.yml)
![Python](https://img.shields.io/badge/python-3.10%2B-blue)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
![MCP](https://img.shields.io/badge/MCP-server-8A2BE2)


An [MCP](https://modelcontextprotocol.io) server for **Griptonite**, the indoor
climbing app. Log attempts and sends, browse routes and grades, and pull your
climbing log into any MCP client — Claude Desktop, Claude Code, Cursor, and
friends. Built in the same spirit as the popular Garmin MCP servers: a thin,
auth-aware wrapper around a fitness backend that has no official public API.

> **Unofficial.** Not affiliated with or endorsed by Griptonite / Cascom Ltd.
> Reverse-engineered from the app's backend for personal use. Respect
> Griptonite's Terms of Service and don't hammer their API.

## What it can do

| Tool | What it does |
|------|--------------|
| `auth_status` | Check whether you're signed in and when the token expires |
| `get_login_url` | Get an OAuth login URL to start the sign-in flow |
| `list_venues` | Find climbing gyms/venues (optional search) |
| `list_routes` | List routes/problems, filter by venue and grade |
| `get_route` | Full detail for one route (e.g. an id from an NFC tag) |
| `list_grades` | Grade systems Griptonite knows (V-scale, Font, ...) |
| `get_profile` | A climber profile (defaults to you) |
| `get_feed` | Recent activity feed (your + crew sends) |
| `get_notifications` | Your notifications |
| `list_workouts` / `list_competitions` / `list_challenges` | Training + events |
| `get_gym_rankings` | Leaderboard for the gyms you've joined |
| `get_world_rankings` | Global ranking across all climbers |
| `get_competition_rankings` | Standings for a competition |
| `get_completed_climbs` | Every climb you've sent, **with tries** |
| `get_projects` | Climbs you're currently projecting, **with tries** |
| `get_all_logs` | All logged entries (sends + attempts) with tries |
| `log_climb` | **Log a send / flash / onsight / attempt / project** on a route |
| `log_attempt` | Shortcut for logging an unsuccessful try |
| `raw_request` | Call any API path with your token (discovery / wiring) |
| `list_known_endpoints` | Show the confirmed API surface |

## How it works

Griptonite's backend lives at `https://api.griptonite.io` and is a Django REST
Framework API guarded by OAuth2 (authorization-code, scope `appuser`). The
endpoint names in `config.py` were confirmed by probing the live API — paths
that answer `401 Unauthorized` exist; `404`s don't. Confirmed groups include routes, grades, venues, feed, `venues/rankings` (gym leaderboards), `profiles/rankings` (world), and `routes/completed` / `routes/projects` / `routes/logs` (your climbs). Auth follows the app's own
browser login: you sign in once, hand back the `code`, and the server caches a
bearer token (default `~/.griptonite/token.json`, gitignored) and refreshes it
transparently.

## Install

```bash
git clone https://github.com/<your-username>/griptonite_mcp.git
cd griptonite_mcp
pip install -e .            # or: pip install -e ".[dev]" for tests
```

Requires Python 3.10+.

## Authenticate (once)

Griptonite has no public API, so there's no API key to paste. Instead you sign
in through the browser once and hand the tool the one-time code, exactly like
the mobile app does. Run:

```bash
griptonite-auth            # or: python -m griptonite_mcp.auth
```

It prints a login URL. Open it, sign into Griptonite, and copy the `code` from
the redirect back into the terminal. The token is cached (and auto-refreshed)
from then on, so you only do this once.

### Catching the code (important)

After you log in, Griptonite redirects to `https://www.griptonite.io/?code=...`
— **but `www.griptonite.io` instantly redirects to `griptonite.io`, and that
redirect strips the `?code` out of the address bar.** That's expected. Grab the
code from the Network tab instead:

1. Open your browser's DevTools (**F12**) and click the **Network** tab.
2. Tick **Preserve log** (so redirect entries aren't cleared).
3. Log in via the printed URL.
4. Filter the list by typing `code`, and click the request named **`?code=...`**
   (the request to `www.griptonite.io`). Its **Request URL** is
   `https://www.griptonite.io/?code=LONGCODE&state=...`.
   - No such row? Click the **`authorize`** request and read its response
     **`location`** header — the code is there.
5. Copy that whole URL (or just the `code` value) and paste it at the prompt.

The code is single-use and expires in about a minute, so grab it promptly. If
you miss it, just re-run `griptonite-auth`. The tool detects a stripped/empty
paste and re-prompts with this reminder.

> **Why not a localhost redirect?** The OAuth client is registered to redirect
> only to `griptonite.io`, so the usual "spin up a local server and auto-capture"
> trick isn't available — hence the one-time manual copy.

## Add to Claude Desktop

Add to `claude_desktop_config.json` (Settings → Developer → Edit Config):

```json
{
  "mcpServers": {
    "griptonite": {
      "command": "python",
      "args": ["-m", "griptonite_mcp"]
    }
  }
}
```

Then restart Claude Desktop and ask things like *"log a V4 flash on route
<id>"* or *"what have I sent at my gym this week?"*.

## Wiring the log endpoint

Reads are fully mapped. The **write** path for logging a climb
(`log_climb` / `log_attempt`) is the one piece that couldn't be confirmed
without capturing an authenticated request, because the mobile app doesn't
expose it publicly. The server's default is `POST /routes/log` — the singular `/routes/log` path
is confirmed to exist — with the route id sent in the body. That's still a best
guess until someone captures the real request; it's trivial to correct:

1. Sign into Griptonite in a browser or proxy the app (Charles / mitmproxy /
   browser DevTools → Network).
2. Log a climb in the app and find the request it fires.
3. Set the real path and, if needed, adjust the JSON field names:
   ```bash
   export GRIPTONITE_LOG_ENDPOINT="/routes/log"   # or the real path you captured
   ```
   For field names, tweak the `payload` dict in `log_climb` (server.py) or the
   `log_climb` method in `client.py`.
4. Meanwhile you can experiment live with the `raw_request` tool, e.g.
   `raw_request("POST", "/routes/123/ticks", body={...})`.

## Example prompts

Once it's wired into your MCP client, try:

- *"What have I sent at my home gym this month, and how many tries did each take?"*
- *"Show my current projects ordered by number of attempts."*
- *"Where do I rank in my gym right now? And globally?"*
- *"Log a V4 flash on route `<id>`."*
- *"Find blue V3–V5 routes at venue `<id>` I haven't tried yet."*

## Configuration

All optional; sensible defaults are baked in. Copy `.env.example` to `.env`.

| Variable | Default | Purpose |
|----------|---------|---------|
| `GRIPTONITE_CLIENT_ID` | (discovered) | OAuth client id |
| `GRIPTONITE_REDIRECT_URI` | `https://www.griptonite.io/` | OAuth redirect |
| `GRIPTONITE_API_BASE` | `https://api.griptonite.io` | API base URL |
| `GRIPTONITE_TOKEN_PATH` | `~/.griptonite/token.json` | Token cache location |
| `GRIPTONITE_LOG_ENDPOINT` | `/routes/log` | Climb-log write path |

## Develop

```bash
pip install -e ".[dev]"
pytest                              # unit tests (no network / creds needed)
python scripts/discover_endpoints.py   # re-probe the live API surface
```

## Project layout

```
griptonite_mcp/
  config.py     # env-driven settings + confirmed endpoint constants
  auth.py       # OAuth2 auth-code flow, PKCE, token cache, refresh
  client.py     # async httpx client with 401-refresh-retry
  server.py     # FastMCP server + tool definitions
scripts/
  discover_endpoints.py   # re-map the API surface
tests/
  test_client.py          # request building, error handling, auth URL
```

## Contributing

Issues and PRs welcome — especially confirming API endpoints/payloads. See
[CONTRIBUTING.md](CONTRIBUTING.md) and the [CHANGELOG](CHANGELOG.md).

## Disclaimer

This is an **unofficial, community project** and is not affiliated with,
endorsed by, or supported by Griptonite / Cascom Ltd. It talks to the same
backend the official app uses, discovered by observing public network behaviour.
Use it for your own data, respect [Griptonite's Terms](https://griptonite.io/legal/terms-conditions/),
and don't hammer the API. If Griptonite ships an official API, prefer that.

## License

MIT — see [LICENSE](LICENSE).

TDQS

A3.7/5.0

Scored across 22 tools

Disambiguation4/5

Most tools target distinct resources (logs, climbs, rankings, etc.), but there is slight overlap between log_attempt and log_climb since log_climb can also record attempts, causing potential confusion for an agent.

Naming Consistency4/5

The majority of tools follow a verb_noun pattern (get_*, list_*, log_*), but 'auth_status' lacks a verb and 'raw_request' uses an adjective-noun pattern, deviating from the predominant convention.

Tool Count4/5

22 tools is slightly above the typical sweet spot of 3-15, but it is justified by the breadth of features (auth, routes, logs, rankings, etc.) and includes a raw_request tool for flexibility.

Completeness4/5

The tool surface covers core workflows like authentication, route browsing, logging climbs, and viewing rankings, with a raw_request tool to handle missing endpoints. Minor gaps exist (e.g., no update/delete for logs), but agents can work around them.

Maintenance

ActivityInactive
ResponsivenessNo issues