Skip to main content
Glama
tumbochka

Untappd MCP

by tumbochka

Untappd MCP

A multi-user Model Context Protocol server for Untappd. It keeps Untappd credentials on the server and exposes a small, explicit tool surface for beer search, profiles and beer history (your own and other users'), wishlists, "has this user had that beer?" lookups, and check-ins.

Current capabilities

  • Streamable HTTP endpoint: POST /mcp

  • OAuth 2.1 authorization-code flow with mandatory PKCE S256; Claude and ChatGPT both connect through it

  • OAuth protected-resource and authorization-server metadata

  • Client ID Metadata Document (CIMD) support, Dynamic Client Registration, and a pre-registered public Claude client

  • Short-lived, audience-bound opaque access tokens and rotating refresh tokens

  • Firebase Auth Google sign-in and a secure server-side browser session

  • Revocable, time-limited personal access tokens as a header-auth alternative to the OAuth flow

  • search_beers (via Untappd's public search index — no shared-quota cost) and get_beer

  • get_my_profile, get_my_wishlist, get_my_beers, get_my_recent_venues, and get_my_friend_feed

  • get_user_profile, get_user_beers, get_user_checkins, and get_user_friends for any Untappd username

  • check_i_had_beer — "have I checked in this beer?" for the connected account, in one API call

  • check_user_had_beer — "has USERNAME ever checked in this beer?", with their rating and first/last dates

  • get_untappd_api_usage — the shared Untappd hourly rate-limit budget and how much is left

  • check_in, with 0–5 quarter-step (0.25) ratings, an optional venue (foursquareId + coordinates from get_my_recent_venues), and message validation

  • toast_checkin — toggle a toast on someone's check-in

  • comment_checkin — post a comment to a check-in

  • Untappd authorization-code connect flow: GET /connect/untappd

  • AES-256-GCM encryption at rest for credentials in Firestore collection untappd_credentials

  • Firebase ID-token verification on every authenticated server request

The server never returns an Untappd access token to an MCP client.

Related MCP server: colacloud-mcp

Identity and authorization

There are three separate credentials. They must never be substituted for one another:

Firebase browser sign-in ──> MCP OAuth server ── MCP access token ──> /mcp
                                              └─ encrypted Untappd access token ──> Untappd API

Firebase Auth identifies the person in the browser and creates a secure HTTP-only session. The MCP authorization server then issues its own access token whose owner is that Firebase uid, whose audience is exactly this server’s /mcp URL, and whose scopes are untappd:read and untappd:write.

As a header-auth alternative to the OAuth flow, the server also issues personal access tokens. They are created only from an authenticated Firebase browser session, are shown once, stored only as SHA-256 hashes, bound to the Firebase uid, expire automatically, and can be revoked at any time. They use the same bearer-token MCP transport.

Endpoint

Purpose

POST /mcp

Streamable HTTP MCP; accepts MCP OAuth bearer tokens and personal access tokens.

GET /health

Unauthenticated liveness endpoint.

GET /.well-known/oauth-protected-resource

Protected-resource metadata. The /mcp suffix variant is also served.

GET /.well-known/oauth-authorization-server

OAuth authorization-server metadata. The /mcp suffix variant is also served.

POST /oauth/register

Dynamic Client Registration, for MCP clients that do not support CIMD.

GET /oauth/authorize

Authorization request, Firebase browser sign-in, then consent.

POST /oauth/token

Authorization-code and refresh-token grants.

GET /tokens

Firebase-authenticated personal access-token management page.

POST /tokens

Creates a personal access token and shows it once.

POST /tokens/revoke

Revokes one of the signed-in user’s personal access tokens.

GET /connect/untappd

Starts the separate Untappd authorization flow for the signed-in Firebase user.

The server keeps only hashes of MCP access, authorization-code, and refresh tokens in Firestore. Refresh tokens rotate; a reused refresh token revokes its whole token family. Untappd access tokens use AES-256-GCM encryption before storage.

Legacy degustation-app migration

When a user opens /tokens, the server first checks for an existing encrypted credential. If none exists, it imports users/{uid}.untappdAccessToken from the old degustation app. When Firebase UID differs, it falls back only to one exact match of the user’s verified Firebase email. The token is validated with Untappd before being encrypted into untappd_credentials/{uid}. Legacy Firestore documents are read-only during this migration and their plaintext token is not deleted.

Untappd connection flow

  1. The user opens /connect/untappd and signs in with Firebase if no browser session exists.

  2. The server creates a signed, ten-minute Untappd OAuth state bound to that Firebase uid, then redirects the user to Untappd.

  3. Untappd redirects to /connect/untappd/callback.

  4. The server exchanges the code, fetches the profile, and stores the encrypted token under untappd_credentials/{firebaseUid}.

Connecting a client

Claude and ChatGPT both connect to https://YOUR_CLOUD_RUN_OR_CUSTOM_DOMAIN/mcp over OAuth:

  1. Add a custom connector pointing at .../mcp and choose OAuth. Both clients discover this server's client metadata automatically (CIMD); Dynamic Client Registration also works.

  2. On first use the client opens this server's authorization page. Sign in with the Google account whose Untappd data the client should use, then approve the untappd:read / untappd:write scopes.

That Google account must already have Untappd connected — if not, open https://YOUR_CLOUD_RUN_OR_CUSTOM_DOMAIN/connect/untappd first and connect it.

Personal access token (alternative)

For header-only auth, or a client that cannot run the OAuth flow:

  1. Open https://YOUR_CLOUD_RUN_OR_CUSTOM_DOMAIN/tokens and sign in with the Google account whose Untappd data you want to use.

  2. Select Create token for Claude, then copy the displayed header value — shown once only.

  3. In the connector settings use .../mcp, set Authentication: None, and add this request header:

Authorization: Bearer pat_…

Use the Revoke button on /tokens immediately if the token is exposed. A token carries the same read/write Untappd scope as an OAuth connection, so the client must still confirm before check-ins.

Local development

Prerequisites: Node.js 22+, Firebase Application Default Credentials, a Firebase project with Google sign-in enabled, and a registered Untappd application.

cp .env.example .env
npm ci
npm test
npm run build
npm start

Configure the values in .env; never commit it. Generate the encryption key with:

openssl rand -base64 32

For local callback testing, register the exact UNTAPPD_REDIRECT_URI with Untappd. Untappd requires HTTPS for API calls, and it expects a non-standard User-Agent for every request.

Deployment

The included Dockerfile is suitable for Cloud Run. Configure these values through Secret Manager:

  • UNTAPPD_CLIENT_SECRET

  • UNTAPPD_TOKEN_ENCRYPTION_KEY

  • CONNECT_STATE_SECRET

Grant the Cloud Run service account Firestore access for untappd_credentials, Secret Manager access for the three runtime secrets, and only firebaseauth.users.createSession plus firebaseauth.users.get for secure Firebase session handling. Keep Firebase service-account credentials and Untappd credentials out of the image and repository.

Set these non-secret runtime variables:

PUBLIC_BASE_URL=https://YOUR_CLOUD_RUN_OR_CUSTOM_DOMAIN
FIREBASE_PROJECT_ID=beer-degustation
FIREBASE_WEB_API_KEY=...
FIREBASE_AUTH_DOMAIN=beer-degustation.firebaseapp.com
FIREBASE_WEB_APP_ID=...
UNTAPPD_CLIENT_ID=...
UNTAPPD_REDIRECT_URI=https://YOUR_CLOUD_RUN_OR_CUSTOM_DOMAIN/connect/untappd/callback
UNTAPPD_USER_AGENT=untappd-mcp/0.1 (support@example.com)
MCP_ALLOWED_ORIGINS=https://claude.ai,https://chatgpt.com
MCP_PERSONAL_ACCESS_TOKEN_TTL_SECONDS=15552000
# Optional: override Untappd's public Algolia search keys only if they rotate.
# Set both or neither; low-sensitivity, plain env is fine.
UNTAPPD_ALGOLIA_APP_ID=
UNTAPPD_ALGOLIA_SEARCH_KEY=

PUBLIC_BASE_URL and UNTAPPD_REDIRECT_URI must use the exact final HTTPS origin. Add that hostname to Firebase Console → Authentication → Settings → Authorized domains before using the OAuth browser login. The browser config fields are Firebase public configuration, not credentials.

Create Firestore TTL policies for expiresAt in these collection groups: mcp_oauth_transactions, mcp_oauth_authorization_codes, mcp_oauth_access_tokens, mcp_oauth_refresh_tokens, and mcp_personal_access_tokens. TTL reduces retained metadata; server-side expiry checks remain mandatory.

Untappd API rate limit

Untappd rate-limits authenticated calls per access token — 100 per rolling hour (unauthenticated client-credential calls share a separate pool). To stay within it:

  • search_beers runs against Untappd's public Algolia beer index and does not spend the quota. It falls back to the Untappd search/beer API (which does) only when Algolia returns an error; every fallback logs "message":"algolia_search_fallback".

  • Every real Untappd API response is recorded from its X-RateLimit-* headers, keyed by the access token that made the call, and emitted as a structured "message":"untappd_api_call" log line (tokenPool, rateLimitRemaining, per-instance counters).

  • get_untappd_api_usage returns lastSeen — the X-RateLimit-Remaining for the connected account's own token (null until this server process has called Untappd with it) — plus instance.* counters that cover every account served by this process. Makes no API call.

  • check_i_had_beer, and check_user_had_beer when the target is another connected user, answer in a single beer/info call. Otherwise check_user_had_beer pages the target's distinct beers (up to maxRequests × 50) and aborts early with stoppedForRateLimit: true once the scanning token's remaining budget drops to ~10.

The single-call path for check_user_had_beer uses the target user's own Untappd token, so a connected user can see another connected user's had-status for a specific beer even if that user's Untappd profile is private and they are not friends. It is limited to accounts connected to this server.

Cloud Run runs several instances, each keeping its own in-memory rate-limit state, so lastSeen is only populated on instances that have recently made a call with that token.

Security notes

  • Close Firestore Rules before deployment. Cloud Run uses the Admin SDK, so it continues to work after browser access is denied. With open rules, an attacker could forge an OAuth token record.

  • Untappd tokens are encrypted before reaching Firestore, but rotate the encryption key with a planned re-encryption migration.

  • The OAuth callback uses a signed and expiring state value to prevent CSRF.

  • OAuth authorization codes are one-time, expire after one minute, and require PKCE S256. Redirect URIs are exact-match registered values.

  • MCP access tokens are short-lived and audience-bound to this server. Untappd tokens are never accepted at /mcp.

  • Personal access tokens are displayed only once, stored as hashes, expire after 180 days by default, and can be revoked from /tokens.

  • check_in, toast_checkin, and comment_checkin are intentionally marked non-idempotent (toast_checkin toggles). The calling model must get user confirmation before invoking them.

  • untappd:write is required for check_in, toast_checkin, and comment_checkin; all other current tools (including get_untappd_api_usage) require untappd:read.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Provides access to the Open Brewery DB API, allowing AI assistants to search for breweries and retrieve detailed information like location, type, and contact details. It enables interactive exploration of a global database containing over 40,000 breweries.
    6 npm
    1
    MIT
  • A
    license
    A
    quality
    Not graded
    maintenance
    Provides access to over 2.5 million US alcohol label records from the TTB via the COLA Cloud API. It enables users to search for labels by brand, barcode, or permit holder and retrieve detailed product information including label images and ABV.
    6
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    An MCP server that interfaces with Untappd's mobile API to search beers, breweries, venues, and user data, and to post check-ins, toasts, and comments using your own account.
    45
    669 npm
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    MCP server for Untappd connectivity, enabling beer search with optional limit, offset, and sorting.
    MIT