Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ strict). Five apps + five shared packages:
| `apps/mcp` | `deckpal-mcp` | **deckpal-mcp** -- MCP server, live and multi-user on cloud; also runs self-host |
| `packages/db` | `@deckpal/db` | Shared Postgres pool + numbered SQL migrations |
| `packages/storage` | `@deckpal/storage` | Shared image path algebra + the `putAsset()` provenance choke point (B1), used by `apps/images` and the cloud image function |
| `packages/agent-tools` | `@deckpal/agent-tools` | The 31 agent tool definitions (reads + writes) shared by `apps/mcp` and Deck-E (`apps/api/src/decke`) -- one definition of what an agent can do, two front-ends |
| `packages/agent-tools` | `@deckpal/agent-tools` | The 32 agent tool definitions (reads + writes) shared by `apps/mcp` and Deck-E (`apps/api/src/decke`) -- one definition of what an agent can do, two front-ends |
| `packages/matching` | `@deckpal/matching` | The scanner's versioned embed input spec (TypeScript + a bit-parity Python mirror), the pgvector text codec, and the SEPARATE identity/variant confidence gates -- imported by `apps/web`'s scan engine, `apps/api` and `tools/embed-catalog`, so all three agree to the bit about what an image is |
| `packages/sim` | `@deckpal/sim` | The Pokémon TCG battle simulator: a pure, seeded rules engine (state as data, card scripts keyed by printed text), CPU pilots, and the batch runner/stats/report behind `POST /decks/simulate` and the `simulate_battles` tool -- no I/O, so it runs in a serverless function or a Web Worker alike |

Expand Down
6 changes: 3 additions & 3 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ served by `apps/images`. Sync jobs run via cron or any scheduler.
## 10. The agent tool layer — one definition, two front-ends

**`packages/agent-tools` (`@deckpal/agent-tools`) is the single definition of
what an agent may do in DeckPal.** 31 tools (20 read, 11 write), plus three
what an agent may do in DeckPal.** 32 tools (20 read, 12 write), plus three
capability-gated `decke_improvement_*` readers served only over MCP, each a
`ToolDefinition`: a zod input schema, `annotations`
(`readOnlyHint` is required in the type, not optional as MCP's own SDK has
Expand All @@ -603,7 +603,7 @@ omitted, as `openWorldHint` does. The MCP adapter (`wireAnnotations` in
`apps/mcp/src/adapters/mcp.ts`) therefore advertises DeckPal's flag OR the
definition's `overwriteHint`, explicitly on every write, `openWorldHint: false`
everywhere, and mirrors the title into `annotations.title` for clients older
than protocol 2025-06-18 — so an MCP client sees 10 of the 11 writes as
than protocol 2025-06-18 — so an MCP client sees 10 of the 12 writes as
destructive while Deck-E still asks only before the four. The adapter also
drops zod's `$schema` line from advertised schemas (2020-12 is MCP's default
dialect) and cuts any single result over 100,000 characters with a visible
Expand Down Expand Up @@ -723,7 +723,7 @@ a set's name.

### MCP server — live and multi-user

`deckpal-mcp`'s 31 tools (20 on a read-only connection) are served to any
`deckpal-mcp`'s 32 tools (20 on a read-only connection) are served to any
signed-up user at
`https://deckpal.app/mcp` (`apps/mcp/src/cloud.ts`), authenticated per-user by
a personal access token (`dsk_…`, SHA-256 hashed, shown once at creation,
Expand Down
14 changes: 13 additions & 1 deletion DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -1262,6 +1262,18 @@ environment variable or infrastructure setting. To confirm on cloud:
`SELECT relrowsecurity FROM pg_class WHERE relname = 'battle_log_bin'`
returns `true`.

Knowledge notes need `089_knowledge_note.sql` (the `knowledge_note` table, the
`knowledge.review` permission seeded to the admin tiers, and the queue and
review functions). 089 turns row-level security on with no policy, so client
roles reach nothing until, on cloud, `090_knowledge_note_rls.sql`
(`@supabase-only`) adds the policies; apply them together. Both use the numbered
runner; neither adds an environment variable. Before 089 is applied,
`get_card` and `rules_lookup` simply show no notes, `/knowledge` answers `503`,
and the API logs `migration 089_knowledge_note is not applied` once per
instance. To confirm on cloud:
`SELECT relrowsecurity FROM pg_class WHERE relname = 'knowledge_note'`
returns `true`.

Only 065 carries `@supabase-only` and is skipped by the normal runner on
self-host. Do not skip 067 there: its cloud-role grants are conditional and its
wallet/economy functions also support the current UUID self-host account.
Expand Down Expand Up @@ -2119,7 +2131,7 @@ it the moment a client no longer needs it.

### Tools

31 tools (20 read, 11 write), plus three `decke_improvement_*` reads served
32 tools (20 read, 12 write), plus three `decke_improvement_*` reads served
only to a connection holding the Deck-E improvement capability. Every tool's
name, arguments, defaults and annotations are in `apps/mcp/TOOLS.md`, which is
generated from the registry and so cannot drift; `apps/mcp/README.md` is the
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Postgres for self-hosters.
addresses the object directly; the image function stays as the fallback that
fills a cold asset); self-host uses a local disk cache with a dedicated image
server. `warm:cloud` warms the whole catalog into the cloud tier.
- **MCP server** ("deckpal-mcp") -- 31 tools (20 read, 11 write) for Claude
- **MCP server** ("deckpal-mcp") -- 32 tools (20 read, 12 write) for Claude
(tested end to end), or ChatGPT, Gemini and other apps with remote MCP
connectors (going by their docs; not yet tested), to query the collection,
catalog, prices, decks, battle logs and
Expand Down Expand Up @@ -116,7 +116,7 @@ pnpm monorepo, deployed on Vercel + Supabase (cloud) or plain Postgres
| `apps/mcp` (`deckpal-mcp`) | **deckpal-mcp** -- MCP server, live and multi-user on cloud |
| `packages/db` (`@deckpal/db`) | Shared Postgres pool + numbered immutable SQL migrations |
| `packages/storage` (`@deckpal/storage`) | Shared image path algebra + the provenance choke point used by `apps/images` and the cloud image function |
| `packages/agent-tools` (`@deckpal/agent-tools`) | The 31 agent tool definitions shared by `deckpal-mcp` and Deck-E, the AI assistant (plus three capability-gated readers served only over MCP) |
| `packages/agent-tools` (`@deckpal/agent-tools`) | The 32 agent tool definitions shared by `deckpal-mcp` and Deck-E, the AI assistant (plus three capability-gated readers served only over MCP) |
| `packages/matching` (`@deckpal/matching`) | The scanner's card-identity embedding: one versioned input spec shared by the phone, the API and the catalogue job, with bit-exact TypeScript/Python parity |
| `packages/sim` (`@deckpal/sim`) | The battle simulator: a pure, seeded Pokémon TCG rules engine plus the batch runner and report behind the read-only `simulate_battles` agent tool |

Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ REST API and the MCP server (`BEGIN` + `set_config('request.jwt.claims', …)` +
returns, so a dropped connection can never be handed to the next request still
carrying a stranger's claims.

**All 31 tools, plus Deck-E's own `preview_card_changes`, reach the
**All 32 tools, plus Deck-E's own `preview_card_changes`, reach the
conversational model; the write half is held by the SDK, not filtered out.** The adapter Deck-E uses
(`apps/api/src/decke/adapters/aisdk.ts`) still *defaults* to
`annotations.readOnlyHint` — never to the verb in a tool's name, because a name
Expand Down
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"test:images": "node --import tsx --test src/images/__tests__/*.test.ts",
"test:stripe-contract": "node ../../scripts/stripe-contract-check.mjs",
"test:integration": "node ../../scripts/test-db-integration.mjs",
"test:pure": "node --import tsx --test src/__tests__/massentry.test.ts src/__tests__/deck-diff.test.ts src/__tests__/list-rules.test.ts src/__tests__/mutations.test.ts src/__tests__/batch-fold.test.ts src/__tests__/soft-delete.test.ts src/__tests__/fingerprint-fold.test.ts src/__tests__/tokens.test.ts src/__tests__/tokensRoute.test.ts src/__tests__/oauthConsent.test.ts src/__tests__/bugs.test.ts src/__tests__/upcoming-sets.test.ts src/__tests__/upcoming-series.test.ts src/__tests__/upcoming-route.test.ts src/__tests__/dbHandle.test.ts src/__tests__/ownerGate.test.ts src/__tests__/billing.test.ts src/__tests__/billing-gate.test.ts src/__tests__/billing-history.test.ts src/__tests__/bodyLimits.test.ts src/__tests__/clientErrors.test.ts src/__tests__/catalog-cache.test.ts src/__tests__/playbooksRoute.test.ts src/dev/__tests__/*.test.ts src/me/__tests__/ownedCards.test.ts",
"test:pure": "node --import tsx --test src/__tests__/massentry.test.ts src/__tests__/knowledgeNotes.test.ts src/__tests__/deck-diff.test.ts src/__tests__/list-rules.test.ts src/__tests__/mutations.test.ts src/__tests__/batch-fold.test.ts src/__tests__/soft-delete.test.ts src/__tests__/fingerprint-fold.test.ts src/__tests__/tokens.test.ts src/__tests__/tokensRoute.test.ts src/__tests__/oauthConsent.test.ts src/__tests__/bugs.test.ts src/__tests__/upcoming-sets.test.ts src/__tests__/upcoming-series.test.ts src/__tests__/upcoming-route.test.ts src/__tests__/dbHandle.test.ts src/__tests__/ownerGate.test.ts src/__tests__/billing.test.ts src/__tests__/billing-gate.test.ts src/__tests__/billing-history.test.ts src/__tests__/bodyLimits.test.ts src/__tests__/clientErrors.test.ts src/__tests__/catalog-cache.test.ts src/__tests__/playbooksRoute.test.ts src/dev/__tests__/*.test.ts src/me/__tests__/ownedCards.test.ts",
"test:pocket-exclusion": "node --import tsx src/__integration__/pocketExclusion.mjs",
"test:decke": "node --import tsx --test src/decke/__tests__/*.test.ts",
"test:export": "node --import tsx --test src/export/__tests__/*.test.ts",
Expand Down
12 changes: 10 additions & 2 deletions apps/api/src/__integration__/improvement.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -253,9 +253,14 @@ async function seedConversation({ conversation, request, operation, seq = 0, cos
]);
}

// Recent, never a fixed date. This leg shares a turn with legs recorded at real time (the backfill test), and a turn's
// latency is its span in integer milliseconds, which overflows past about 24.8 days: a fixed 2026-09-28 started failing
// every run from 2026-10-23.
const LEG_STARTED = new Date(Date.now() - 60_000).toISOString();
const LEG_FINISHED = new Date(Date.now() - 59_000).toISOString();
const validLeg = (answer = 'redacted answer') => ({
asked: 'redacted question', answered: answer, model_id: 'fixture/model', provider: 'fixture',
started_at: '2026-09-28T18:00:00.000Z', finished_at: '2026-09-28T18:00:01.000Z', latency_ms: 1000,
started_at: LEG_STARTED, finished_at: LEG_FINISHED, latency_ms: 1000,
input_tokens: 123, output_tokens: 27, cache_read_tokens: 44, cache_write_tokens: 62, reasoning_tokens: 13,
cost_usd: 0.00137, cost_coverage: 'complete', cost_source: 'provider_reported', status: 'completed', finish_reason: 'stop',
build_sha: 'fixture-sha', build_pr: 278, error: { owner: 'John%20Smith' },
Expand Down Expand Up @@ -1269,8 +1274,11 @@ try {
const nextDay = new Date(Date.parse(day + 'T00:00:00Z') + 86_400_000).toISOString().slice(0, 10);
const dayBucket = await token(owner, tokenId, (c) => data(c,
'SELECT public.decke_improvement_list($1::jsonb,NULL,20) data', [JSON.stringify({ from: day, to: nextDay })]));
assert.ok(dayBucket.items.length > 1);
// The day filter keeps exactly the chats in its range: every one dated that day is in it, none from outside it.
// (Not "more than one": how many fixtures share a day depends on when the suite runs; see LEG_STARTED.)
assert.ok(dayBucket.items.some((item) => item.id === derivedShared));
for (const item of list.items.filter((i) => i.date === day)) assert.ok(dayBucket.items.some((b) => b.id === item.id), item.id);
for (const item of dayBucket.items) assert.ok(item.date >= day && item.date <= nextDay, item.date);
assert.equal(typeof improvementRoute.listFilters, 'function');
const routeFilters = improvementRoute.listFilters({ min_cost: '0.01', max_cost: '2.00' });
assert.deepEqual(routeFilters, { min_cost: 0.01, max_cost: 2 });
Expand Down
Loading
Loading