Skip to content

MCP Setup

cheyras edited this page Oct 17, 2026 · 15 revisions

DeckPal — Connecting an AI Assistant (MCP)

DeckPal speaks the Model Context Protocol, so an assistant can answer questions about your collection: what you own, what a set still needs, what a deck costs, how it has been performing. This is DeckPal's headline feature — a multi-user MCP server, so each collector connects their own assistant to their own data. The cloud deployment serves it at https://deckpal.app/mcp (Streamable HTTP) for every signed-up user; a self-host deployment runs the same server as its own process. This page is the setup guide; see DEPLOYMENT.md in the repo for the canonical copy.


Administration, features and personal-account boundary (2026-09-15)

The shared MCP catalog remains 24 tools (13 read/11 write); no administrative tools were added. Owner and Superadmin are distinct browser-session authorities, and neither expands a connector token's scope. PAT/OAuth tokens cannot call admin, wallet/purchase, feature-preference or conversation-sharing APIs. Connector collection/deck access remains scoped to its account and current suspension.

Every account has one canonical role. Contributor has development-tool capabilities without Administration, while retaining ordinary own profile, subscription and credit-wallet self-service in an active browser session. Product Scanner/Deck-E access comes from lifecycle and opt-in, not editable role grants or preview hostnames. Disabled stops new provider starts, including Owner.

Deck-E independently logs server request/provider-attempt metadata and honors its flat credit/daily policy. Owner-only unlimited/markup overrides do not grant MCP administration or bypass feature, suspension, debt, holds or budgets. Support subscriptions and gifts do not purchase AI credits.

Conversation sharing is a separate default-off browser preference. Current exchange-start and present consent must match before an administrator reads optional current-message/assistant content. Withdrawal hides prior shared text; off-on never restores it. Own history and metadata are distinct, and no prompt/ tool context is added to general telemetry or connector tools. Private APIs remain no-store and outside anonymous service-worker caching.

Suspension/token revoke-all still cover API, token resolver, standalone MCP context and restrictive data policies; OAuth codes are consumed as well. Mint/exchange and revoke-all coordinate through commit. Reactivation does not revive credentials. Already-running work and previously downloaded content cannot be recalled. Administrative UI routes/actions remain outside Deck-E's navigation/click allowlists.

1. Connect

Works with claude.ai, ChatGPT, Gemini, Claude Code, or any client that speaks the MCP Authorization spec (OAuth 2.1 + PKCE + dynamic client registration — apps/api/src/oauthServer.ts).

  1. In your client, add a custom MCP connector pointed at https://deckpal.app/mcp and choose Connect (not a manual-header option).
  2. Your client registers itself, then opens https://deckpal.app/authorize in a browser tab. Sign in to DeckPal if you aren't already — an account is required (there is no anonymous or auto-created access).
  3. Approve the consent screen — it names the client asking and exactly what it can do (read/write your collection, decks, lists, battle logs; not your password, not your account settings).
  4. You're bounced back to the client, already connected. No token to copy.

Under the hood, approving mints an ordinary personal access token (below) named after the client — it shows up in Profile → Agent access exactly like one you created by hand, with the same Revoke button.

2. If your client doesn't support MCP OAuth — a personal access token

Some clients (or older versions) only take a static URL or header. For those:

  1. Sign in at https://deckpal.app and open Profile (the avatar, top right).
  2. Scroll to Agent access and press New token.
  3. Name it after the client — e.g. claude.ai or Claude on my laptop — and press Create token.
  4. Copy the value immediately. DeckPal stores only a SHA-256 hash, so the token is shown exactly once and can never be recovered. Alongside it you also get a personal connector URL of the form https://deckpal.app/mcp/dsk_… — copy that too.

Tokens are listed afterwards by their dsk_… prefix with their creation and last-used dates, and can be revoked from the same panel at any time — same as an OAuth-connected client, because it's the same underlying credential.

A · If the dialog has a "Request headers" section

  1. Remote MCP server URL: https://deckpal.app/mcp
  2. Open Request headers. Choose the header name authorization and set the value to Bearer <your token> — the word Bearer, one space, then the token. Mark it Required.
  3. Click Add.

B · If there is no header field either — use your personal connector URL

  1. Remote MCP server URL: paste https://deckpal.app/mcp/dsk_… (the personal connector URL from step 4 above).
  2. Add no headers. Click Add.

That URL contains your token, so treat the whole string like a password: don't paste it into a screenshot, a shared doc, or a bug report. It is revocable and scoped to exactly one user — revoking the token kills the URL. The token is in the URL path, never a query parameter (the MCP authorization spec forbids credentials in the query string).

Avoid mixing the two methods for the same client: pointing a client at a personal-connector-URL and letting it separately attempt OAuth has been observed to make some clients treat the token in the URL's last path segment as if it were an OAuth client_id, which can never resolve (it was never registered as one) and surfaces as "unknown client_id." If you hit that, delete the connector and re-add it with the bare https://deckpal.app/mcp URL via Connect (§1) instead.

3. Check that it works

Start a new chat, enable DeckPal in the tools menu, and ask:

what is my collection worth, and which set am I closest to finishing?

You should get your own numbers back. The token's Last used date in Profile → Agent access updates within a minute.

4. Claude Code

claude mcp add --transport http deckpal https://deckpal.app/mcp

Claude Code runs the OAuth flow itself and opens /authorize in your browser. To use a static token instead: claude mcp add --transport http deckpal https://deckpal.app/mcp --header "Authorization: Bearer <your token>".

claude mcp list should then print:

deckpal: https://deckpal.app/mcp (HTTP) - ✔ Connected

Remove it with claude mcp remove deckpal.

5. If it doesn't connect

  • Use https://deckpal.app exactly — not www.deckpal.app. The www host 308-redirects to the apex, and a redirect to a different host silently drops the Authorization header (and breaks the OAuth redirect_uri exact-match check).
  • Landing on /authorize and seeing "Invalid connection request" means the client sent a malformed request (missing client_id or PKCE parameters) — usually a client that doesn't actually implement MCP OAuth yet. Use the personal access token instead (§2).
  • Landing on /authorize and seeing "unknown client_id" — see the note at the end of §2; reconnect using the bare URL + Connect instead of a personal-connector-URL.
  • "Couldn't reach the MCP server" or an authorization failure with a manual token almost always means the token is missing, truncated, or revoked. A token cannot be shown twice, so a partial copy is unrecoverable — create a fresh one and re-paste.
  • In option A above, include the word Bearer and one space before the token. claude.ai sends the header value exactly as typed and adds no scheme of its own.

6. Revoking

Profile → Agent access → Revoke. It takes effect immediately, on every client, whether it connected via OAuth or a manual token — both are the same api_token row. The row stays in the list, struck through and marked Revoked, so you can see it happened. Then delete the connector in your client or reconnect to get a new one.

Any other MCP client

Point it at https://deckpal.app/mcp over Streamable HTTP and either let it run OAuth discovery (.well-known/oauth-protected-resource → .well-known/oauth-authorization-server → register → /authorize → /token), or give it an Authorization: Bearer <token> header, or point it at https://deckpal.app/mcp/<token> with no header at all.

What the token grants

The token acts as you, limited to your own data. Whoever holds it can read and change your collection, lists, decks and battle logs — the same things you can do while signed in — and nothing else: every query it makes runs inside your row-level-security context, so it cannot see another user's rows. It cannot change your password, and it cannot create or revoke tokens (that needs a real browser session). Treat it like a password, and revoke it the moment a client no longer needs it.

The endpoints that mint or revoke a token, or approve an OAuth client, are REST routes (POST /tokens, the OAuth consent decision POST /oauth/authorize/decision, and /avatar) and are rate-limited per authenticated user — /tokens 20/min, /oauth 30/min, /avatar 10/min — refused with 429 + Retry-After (seconds) before any database connection is acquired. The MCP transport at /mcp itself is served by a separate function and is not covered by that REST guard; the token's own RLS scoping (above) is unchanged.

Tools

28 tools (17 read, 11 write), plus three capability-gated decke_improvement_* readers shown only to a token holding the Deck-E improvement capability. The list is no longer written by hand anywhere: apps/mcp/TOOLS.md is generated from the server's own tools/list (pnpm mcp:tools-doc), with every argument, default and annotation, and a test fails when it or any doc's tool count drifts. apps/mcp/README.md covers connecting (Claude, Claude Code, ChatGPT, any client with a token), tokens, read vs write, and example sessions. See apps/mcp/SPEC.md for the design contract.

What a client is told about each tool (2026-10-12). Annotations follow the MCP spec: every tool advertises openWorldHint: false (the spec default is true) and annotations.title; every write states destructiveHint in the spec's sense ("anything but purely additive" — 10 of the 11, all but add_battle_log) and idempotentHint. Every write previews first (dry_run defaults to true) except deck_strategy, which says it writes at once. Opponent screen names and card names from pasted logs are printed as quoted values when they do not look like ordinary names, raw logs are fenced as third-party text, and the server instructions say such text is data, never instructions. List-shaped reads page (raw battle logs 3 a page, one list's items 100, the deck index 100, one operation's changes 50); pnpm mcp:audit measures every tool's size and result against a seeded database, and the database CI job enforces budgets.

Deck questions with numbers (read-only, 2026-10-10). Both take a saved deck (id or name) or an unsaved list in check_deck's shapes, so a change can be tested before it is saved, and both are seeded, so asking twice gives the same answer. deck_odds is draw math: opening-hand, Prize and seen-by-turn-N odds from a seeded shuffle of the kept hand, with a 95% margin per value and the exact value where one exists; it plays no cards. simulate_battles plays the deck against up to 8 of your decks (default: up to 6 of your other decks) with a CPU pilot on both sides (@deckpal/sim), in paired games with seats swapped, and reports win rates with Wilson 95% intervals and n, setup speed, Prize-takers and loss patterns, and every card the engine only approximates or cannot play. Its first and last line say the games are simulated by a bot, not real results, and nothing is stored.

Where they live. In production, the tools are defined once in the shared packages/agent-tools workspace package (packages/agent-tools/src/tools/*.ts, @deckpal/agent-tools) and surfaced through two adapters: the MCP adapter (apps/mcp/src/adapters/mcp.ts, the only file that knows MCP exists — it walks allTools() and calls server.registerTool(...) per tool) and Deck-E's AI SDK adapter (apps/api/src/decke/adapters/aisdk.ts). A tool added or changed for Claude over MCP appears for Deck-E in the same commit, and vice versa. card_price_history (added 2026-09-12) is the newest read-only tool: it pulls per-card OHLC price history through the existing GET /cards/:cardId/prices endpoint, with the same rollup MAY/MAY NOT interpretation contract the REST route enforces. get_card still returns current prices.

Price history is paginated in text so all printing variants can be retrieved through MCP and conversational Deck-E. Start with offset: 0 (the default); each page contains complete OHLC records and printing identity within 5500 characters. When it says next_offset=<integer>, call card_price_history again with that nonnegative safe integer as offset and the same card_id, range, and currency. Stop at next_offset=none. The offset selects tool output only; each call still makes one request to the existing prices endpoint. Empty history is distinct from an offset past the available records. Offsets count points in API order, plus one record per empty variant. A single complete record too large for the page budget returns an explicit error instead of partial history.

Self-host

The MCP server is a separate long-lived process (apps/mcp), not part of the API. It binds 127.0.0.1:3704 and is gated by a single shared secret rather than per-user tokens, because a self-host deployment has one user. The OAuth "Connect" flow above is cloud-only (it mints api_token rows tied to a Supabase user, which self-host has no concept of):

pnpm --filter deckpal-mcp build
DECKPAL_MCP_KEY=$(openssl rand -hex 32) node apps/mcp/dist/index.js
Variable Meaning
DECKPAL_MCP_KEY Shared secret required in the x-brain-key header. Startup fails without it.
MCP_ALLOWED_HOSTS Comma-separated Host allowlist (DNS-rebinding protection). Default 127.0.0.1,localhost.
DECKPAL_MCP_PORT Listen port. Default 3704.
DECKPAL_API_BASE Where the REST API lives. Default http://127.0.0.1:3700/deckpal/api.

Expose it through your reverse proxy at whatever path you like, add the x-brain-key header there (or have the client send it), and point your MCP client at that URL. Personal access tokens created in the UI still work as Authorization: Bearer credentials against the REST API itself.

Self-hosters who want the per-user endpoint instead can run node apps/mcp/dist/cloud.js (defaults to 127.0.0.1:3705), which is the same code the cloud function serves and expects Authorization: Bearer dsk_….



Write safety (2026-08-19)

Three things changed about how an assistant writes to your collection, all of them consequences of one incident: a 99-item log_cards call reported "the connector's server isn't responding" three times while every write committed, the agent retried, and quantities inflated up to 4x.

Retrying is now safe. Every batch carries an idempotency key derived from its own resolved contents. Re-sending after an error returns the ORIGINAL result and writes nothing; the output says REPLAYED on its first line. The key is bucketed by time (about 15 minutes), so a retry collides while genuinely pulling the same cards again next month applies normally — and is flagged, so you can undo it if it was a retry after all.

A timeout tells the truth. If the call fails ambiguously, the tool asks the mutation log what actually landed and reports that, instead of returning an error that hides committed work.

Everything is undoable. mutation_history shows what any operation did, with a before/after snapshot per card. revert undoes a whole operation, one change, a time window, or every change to one card — dry-run first, always. It refuses rather than half-applying where an exact undo is impossible (the original change hit the zero floor, or something later asserted an absolute quantity), and it says why. The revert is itself logged, so it can be reverted.

Deleting a list, a deck or a battle log is reversible too: it moves to Recently deleted (on the Lists and Decks pages, and under a deck's battle logs) and is kept until you choose Delete forever.

Since 2026-10-12 (PR #349):

  • A deck's card list and every battle-log add, edit and delete are recorded with their full before/after state, so revert puts an earlier list back as a new version (even an edit that amended an unplayed version in place) and undoes battle-log changes. delete_battle_log takes restore and purge; battle_logs takes deleted: true to list the bin.
  • The other writes a retry would double (add_battle_log, a deck revert, a list create or bulk add, a forced revert) carry a retry key too: the same request within 30 minutes (up to an hour) returns the first result.
  • deck_strategy previews a rewrite by default over MCP; dry_run: false saves.

Cart links

set_cart builds TCGplayer Mass Entry links from a set, a saved list, or an explicit set of cards. Use list_id when you have already filtered a list — otherwise the cart is re-derived from the set and the cards you excluded come straight back.

Lines name the TCGplayer product directly (<qty>-<productId>). Card names are not unique inside a set once it reprints them as Illustration and Special Illustration rares, and Mass Entry is all-or-nothing — one unmatched line adds nothing at all, which is why a card with no TCGplayer product is now reported rather than guessed at.

Deck-E chat and MCP card logging (2026-09-23)

MCP log_cards retains dry_run: true by default; explicit false applies changes. Only DeckPal chat uses apply-intent log_cards, signed approval, and read-only preview_card_changes; clients must not assume the chat schema for MCP. No new MCP alias is defined.

Last updated by Claude Opus 5.5 on behalf of @cheyras -- 2026-10-17

Clone this wiki locally