Repository navigation
MCP Setup
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.
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.
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).
- In your client, add a custom MCP connector pointed at
https://deckpal.app/mcpand choose Connect (not a manual-header option). - Your client registers itself, then opens
https://deckpal.app/authorizein a browser tab. Sign in to DeckPal if you aren't already — an account is required (there is no anonymous or auto-created access). - 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).
- 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.
Some clients (or older versions) only take a static URL or header. For those:
- Sign in at https://deckpal.app and open Profile (the avatar, top right).
- Scroll to Agent access and press New token.
- Name it after the client — e.g.
claude.aiorClaude on my laptop— and press Create token. - 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
- Remote MCP server URL:
https://deckpal.app/mcp - Open Request headers. Choose the header name
authorizationand set the value toBearer <your token>— the wordBearer, one space, then the token. Mark it Required. - Click Add.
B · If there is no header field either — use your personal connector URL
- Remote MCP server URL: paste
https://deckpal.app/mcp/dsk_…(the personal connector URL from step 4 above). - 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.
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.
claude mcp add --transport http deckpal https://deckpal.app/mcpClaude 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.
- Use
https://deckpal.appexactly — notwww.deckpal.app. Thewwwhost 308-redirects to the apex, and a redirect to a different host silently drops theAuthorizationheader (and breaks the OAuth redirect_uri exact-match check). - Landing on
/authorizeand seeing "Invalid connection request" means the client sent a malformed request (missingclient_idor PKCE parameters) — usually a client that doesn't actually implement MCP OAuth yet. Use the personal access token instead (§2). - Landing on
/authorizeand 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
Bearerand one space before the token. claude.ai sends the header value exactly as typed and adds no scheme of its own.
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.
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.
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.
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.
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_….
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
revertputs 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_logtakesrestoreandpurge;battle_logstakesdeleted: trueto list the bin. - The other writes a retry would double (
add_battle_log, a deck revert, a list create or bulk add, a forcedrevert) carry a retry key too: the same request within 30 minutes (up to an hour) returns the first result. -
deck_strategypreviews a rewrite by default over MCP;dry_run: falsesaves.
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.
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