Skip to content
Draft
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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,10 @@ __pycache__/

# Ruff
.ruff_cache/

# AI Config
.claude/
CLAUDE.md

# ngrok
ngrok.yml
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,9 +227,10 @@ Include this in the environment variables:

```
BASE_URL=https://your-server.com
API_BASE_URL=https://your-api.example.com
```

This will be used to generate the HTML for the widgets so that they can serve static assets from this hosted url.
This will be used to generate the HTML for the widgets so that they can serve static assets from this hosted url. `API_BASE_URL` is used by client widgets to build fully-qualified API URLs (for example, the Cards Against AI game event stream).

## Contributing

Expand Down
36 changes: 35 additions & 1 deletion build-all.mts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { build, type InlineConfig, type Plugin } from "vite";
import dotenv from "dotenv";
import react from "@vitejs/plugin-react";
import fg from "fast-glob";
import path from "path";
Expand All @@ -7,6 +8,8 @@ import crypto from "crypto";
import pkg from "./package.json" with { type: "json" };
import tailwindcss from "@tailwindcss/vite";

dotenv.config({ path: path.resolve(process.cwd(), ".env.local") });

const entries = fg.sync("src/**/index.{tsx,jsx}");
const outDir = "assets";

Expand All @@ -17,6 +20,7 @@ const GLOBAL_CSS_LIST = [path.resolve("src/index.css")];
const targets: string[] = [
"todo",
"solar-system",
"cards-against-ai",
"pizzaz",
"pizzaz-carousel",
"pizzaz-list",
Expand All @@ -27,6 +31,13 @@ const targets: string[] = [
"kitchen-sink-lite",
"shopping-cart",
];
const cliTargetIndex = process.argv.indexOf("--target");
const cliTarget = cliTargetIndex !== -1 ? process.argv[cliTargetIndex + 1] : null;
if (cliTarget) {
targets.length = 0;
targets.push(cliTarget);
}

const builtNames: string[] = [];

function wrapEntryPlugin(
Expand Down Expand Up @@ -168,18 +179,41 @@ console.groupEnd();
console.log("new hash: ", h);

const defaultBaseUrl = "http://localhost:4444";
const baseUrlCandidate = process.env.BASE_URL?.trim() ?? "";
const baseUrlCandidate = (
process.env.VITE_BASE_URL ??
process.env.BASE_URL ??
""
).trim();
const baseUrlRaw = baseUrlCandidate.length > 0 ? baseUrlCandidate : defaultBaseUrl;
const normalizedBaseUrl = baseUrlRaw.replace(/\/+$/, "") || defaultBaseUrl;
console.log(`Using BASE_URL ${normalizedBaseUrl} for generated HTML`);

const defaultApiBaseUrl = "http://localhost:8000";
const apiBaseUrlCandidate = (
process.env.VITE_API_BASE_URL ??
process.env.API_BASE_URL ??
""
).trim();
const apiBaseUrlRaw =
apiBaseUrlCandidate.length > 0 ? apiBaseUrlCandidate : defaultApiBaseUrl;
const normalizedApiBaseUrl =
apiBaseUrlRaw.replace(/\/+$/, "") || defaultApiBaseUrl;
const appUrlConfigJson = JSON.stringify({
apiBaseUrl: normalizedApiBaseUrl,
assetsBaseUrl: normalizedBaseUrl,
});
console.log(`Using API_BASE_URL ${normalizedApiBaseUrl} for generated HTML`);

for (const name of builtNames) {
const dir = outDir;
const hashedHtmlPath = path.join(dir, `${name}-${h}.html`);
const liveHtmlPath = path.join(dir, `${name}.html`);
const html = `<!doctype html>
<html>
<head>
<script>
window.__APP_URL_CONFIG__ = ${appUrlConfigJson};
</script>
<script type="module" src="${normalizedBaseUrl}/${name}-${h}.js"></script>
<link rel="stylesheet" href="${normalizedBaseUrl}/${name}-${h}.css">
</head>
Expand Down
23 changes: 23 additions & 0 deletions cards_against_ai_server_node/ANSWER_DECK_GUIDANCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Cards Against AI Answer Deck Guidance

Goal
Create an answer deck with at least 100 AnswerCards. Each answer card is a short,
punchy phrase meant to complete or respond to a prompt card in a funny way.

Tone and Content
- Aim for humor: clever, absurd, irreverent, or unexpected.
- Keep it short: usually a word or short phrase (1-8 words).
- Use a mix of styles: wordplay, deadpan, pop culture, current events, and
relatable everyday situations.
- Include variety: people, places, objects, actions, abstract ideas, and
references.
- Avoid slurs or hateful content; edgy is fine, harmful is not.

Relevance Tips
- Sprinkle in recent or evergreen pop culture references.
- Include a few topical references that would feel current to most players.
- Balance niche references with broad ones so most prompts have good options.

Usage
These cards are dealt to players and submitted to the judge each round. A strong
deck makes it easy to pick funny responses across many prompts.
43 changes: 43 additions & 0 deletions cards_against_ai_server_node/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Cards Against AI — MCP Server (Node)

MCP Apps backend that drives a card game through ChatGPT's model while keeping a real-time widget in sync. Single server serves both MCP API and widget assets.

## Quick Start

```bash
pnpm install # from repo root
cd cards_against_ai_server_node
pnpm run dev # builds widget + starts server on :8000
```

The server serves widget assets and MCP endpoint from the same port (8000).

### With ngrok

```bash
echo 'BASE_URL=https://your-domain.ngrok.app' > .env.local # in repo root
ngrok http 8000 --domain your-domain.ngrok.app
cd cards_against_ai_server_node && pnpm run dev
```

Single tunnel, single server.

### Scripts

| Script | Description |
|--------|-------------|
| `pnpm run dev` | Build widget + start server |
| `pnpm run build` | Build widget only |
| `pnpm run build:check` | Build widget + typecheck app + server |
| `pnpm start` | Start server (no build) |

## Key MCP Apps Concepts

- **Tool response structure** — `buildGameToolResponse` shows the three data channels: `_meta` (widget binding), `content` (model-visible text), and `structuredContent` (widget-visible data).
- **Widget session binding** — `openai/widgetSessionId` ties all tool responses to the same widget iframe. Without it, each tool call spawns a new widget.
- **Resource registration** — Widget HTML is served as an MCP resource so ChatGPT can render it. CSP metadata controls which domains the sandboxed iframe can access.
- **Rules resources** — `rules://` URIs provide context documents the model reads before acting. They inform behavior, not UI.
- **Tool annotations** — `toolAnnotations` hint to ChatGPT whether to show confirmation dialogs (readOnlyHint, destructiveHint, openWorldHint).
- **Stateless transport** — `createCardsAgainstAiServer` creates a fresh McpServer per request. Game state lives in a Map, not in the MCP session.
- **SSE for real-time updates** — Custom SSE endpoint pushes game state to the widget on every mutation, separate from the MCP protocol.
- **Zod input schemas** — `registerAppTool` accepts Zod shapes, not JSON Schema. The SDK converts them automatically.
Loading