---
name: publish-to-pitchstation
description: Publish an HTML report or PDF you just drafted to PitchStation and return a secure, tracked share link. Use after generating a one-pager/report/deck the user wants to share, or whenever they ask to "publish to pitchstation", "share via pitchstation", "get a pitchstation link", etc. Supports access modes (link/account/recipient), expiry, max-views, password, watermark, allowlist.
---

# publish-to-pitchstation

This skill publishes a generated document to a self-hosted PitchStation server and returns a secure `/s/<slug>` link. Every share comes with per-slide analytics (for decks), per-open audit log, optional password, expiry, watermark, and revoke control.

## When to use

Invoke this skill when the user wants to:

- Share a one-pager, report, or AI-drafted HTML document you just produced.
- Publish a PDF you have on disk and get a tracked link.
- Share an existing PitchStation deck (one already in `output/<deckId>/`).
- "Publish to PitchStation", "share this", "get me a link for this", etc.

## How it works

You have three MCP tools available (registered as the `pitchstation` MCP server):

- **`pitchstation_publish`** — publishes content and returns a `/s/<slug>` URL.
- **`pitchstation_list`** — lists shares owned by the caller (optionally filtered by deckId).
- **`pitchstation_revoke`** — kills a share by slug.

### Source — exactly one of:

| Argument | When to use |
|---|---|
| `htmlContent` | A raw HTML string. Most common — pass the AI-drafted HTML directly. |
| `pdfBase64` | Base64-encoded PDF bytes. Use when the user provides a `.pdf` file. |
| `docxBase64` | Base64-encoded Word (`.docx`) bytes. Stored intact; the viewer shows a branded download card (Office can't render inline) and the open is tracked. |
| `pptxBase64` | Base64-encoded PowerPoint (`.pptx`) bytes. Same download-card behavior as `.docx`. |
| `deckId` | An existing PitchStation deck on disk (from a prior generation). |
| `files: [...]` | **Multi-file bundle**. Each entry becomes a tab in the viewer sidebar. Mix HTML, PDF, Word (`.docx`), PowerPoint (`.pptx`), images, Markdown, and text freely. One URL / one password / one analytics scope for the whole bundle. Use when the user asks to "publish these three files together" or "package the deck + appendix + summary into one share". |

### Access modes

- `account` (default — **safe**): only signed-in PitchStation users. Add `aclUsernames: ["alice", "bob"]` to restrict to specific users.
- `link`: anyone with the URL — fastest to share. Pair with `expires_in` + `password`.
- `recipient`: pass `recipients: ["alice@x.com", "bob@x.com"]`. Each gets their own magic link.

### Controls (any combination)

- `expires_in`: `"24h"`, `"7d"`, `"30d"`, `"never"`, or an ISO timestamp.
- `max_views`: auto-expire after N opens.
- `password`: `true` to auto-generate a memorable password, or pass a custom string.
- `watermark`: stamps viewer identity + timestamp onto HTML pages.
- `downloads`: enable downloading the file (off by default).

## Plain-language mapping

When the user says…

| User phrase | What to pass |
|---|---|
| "share with the team" | `access: "account"` |
| "link anyone can open" | `access: "link"` |
| "send to alice and bob" | `access: "recipient", recipients: ["alice@…", "bob@…"]` |
| "expires in a week" | `expires_in: "7d"` |
| "one-time link" | `max_views: 1` |
| "with a password" | `password: true` |
| "watermark with the viewer name" | `watermark: true` |
| "allow downloads" | `downloads: true` |
| "let people forward it" / "viewers can reshare" | `allow_forward: true` — viewers mint their own tracked links; opens roll up to the origin and revoke cascades |
| "ask who's viewing" / "viewers must give their name" | `require_name: true` — link-mode viewers enter a name before viewing; it lands in the open log and watermark |
| "share with alice and email her" | `access: "account", viewers: ["alice@x.com"]` (emails or usernames) — the list IS the allowlist, and everyone on it is auto-emailed the link, **including the password** for protected shares; signed-in viewers on the list skip the password entirely. (`aclUsernames` = same allowlist without the email.) |
| "invoice Harbour C for the website, HK$6,800 NET 14" | `pitchstation_invoice_create` with `items:[{description, unit_cents}]` (+ `send: true` to email it immediately — password included for external clients) |
| "bundle these three together" / "publish all of them as one share" / "package the deck + the PDF appendix + the markdown notes" | `files: [{title, …}, {title, …}, {title, …}]` |
| "translate / read in Chinese / multi-language" | Nothing — every HTML share auto-shows a language picker (EN / 中文 / ES / FR / DE / AR / PT / HI). Mention it to the user. |

## Examples

### Publish an AI-drafted HTML report (safe default)

```
pitchstation_publish:
  htmlContent: "<!doctype html><html><head><title>Q1 Update</title></head><body>…</body></html>"
  title: "Q1 Update"
```

→ Returns a `/s/<slug>` link gated by login.

### Public link with password + expiry

```
pitchstation_publish:
  htmlContent: "<html>…</html>"
  access: "link"
  expires_in: "7d"
  password: true
  watermark: true
```

→ Returns the URL + the one-time password. Tell the user to share both.

### Recipient mode (per-person magic links)

```
pitchstation_publish:
  htmlContent: "<html>…</html>"
  access: "recipient"
  recipients: ["alice@example.com", "bob@example.com"]
```

→ Returns a per-email magic-link table. Forward each one to the matching recipient.

### Publish a PDF

```
pitchstation_publish:
  pdfBase64: "<base64-encoded PDF bytes>"
  title: "Board Pack"
  access: "account"
  aclUsernames: ["ceo", "cfo", "board-chair"]
```

### Multi-file bundle (deck + financials + notes in one share)

```
pitchstation_publish:
  title: "Acme Q3 board pack"
  access: "account"
  aclUsernames: ["ceo", "cfo", "board-chair"]
  expires_in: "30d"
  files:
    - title: "Board deck"
      htmlContent: "<!doctype html>…full deck…"
      primary: true
    - title: "Q3 financials"
      pdfBase64: "<base64 PDF>"
    - title: "Risks & notes"
      markdown: "# Key risks\n\n- Supply chain…\n- Pricing pressure…"
    - title: "Hero image"
      imageBase64: "<base64 PNG>"
```

→ Returns ONE `/s/<slug>` URL. Viewer sees a sidebar with four files and clicks to switch. Per-file format icons. All four count under the same `max_views` / password / expiry. Each open is logged once.

Pick **exactly one** file as `primary: true` (or omit — the first entry is primary). The primary file is what the legacy single-file viewer falls back to if someone bypasses the bundle chrome.

### List your shares

```
pitchstation_list:
  (no args, or deckId: "1234-…" to filter)
```

### Revoke

```
pitchstation_revoke:
  slug: "abc123XY"
```

## Configuration

The MCP server reads two env vars:

- `PITCHSTATION_URL` — defaults to `http://localhost:3018`. Set to your prod URL in production.
- `PITCHSTATION_TOKEN` — a Personal Access Token (PAT). Create one at `<URL>/tokens.html` and store the `pst_…` value somewhere the MCP host can reach (env file, secrets manager).

Register the MCP server in your Claude Code config (`~/.claude/mcp_servers.json` or via `claude mcp add`):

```json
{
  "mcpServers": {
    "pitchstation": {
      "command": "node",
      "args": ["/absolute/path/to/generator/mcp/pitchstation-mcp-server.js"],
      "env": {
        "PITCHSTATION_URL": "http://localhost:3018",
        "PITCHSTATION_TOKEN": "pst_…"
      }
    }
  }
}
```


## Spaces — one evolving link for many documents

Three more tools manage **Spaces**: branded, tracked collections of existing shares behind a single `/space/<slug>` URL (a deal room, a client hub, a portfolio).

- **`pitchstation_space_create`** — `{ title, description?, access?, password?, expires_in?, require_name? }` → space URL (+ password if generated).
- **`pitchstation_space_add`** — `{ spaceSlug, shareSlug, section? }` adds an existing share to the space.
- **`pitchstation_space_list`** — lists your spaces with URLs and doc counts.

### Bundle vs Space — the decision rule

| The user wants… | Use |
|---|---|
| "send these 3 files exactly as they are" (frozen snapshot, one share) | `files: [...]` bundle |
| "one link I can keep adding to / updating" (evolving engagement) | a **Space** |
| "a data room for the raise" / "client hub" / "everything in one place" | a **Space** |

### Typical flow

```
1. pitchstation_publish  (deck)      → slug A
2. pitchstation_publish  (financials)→ slug B
3. pitchstation_space_create { title: "Acme · Series A", password: true }
4. pitchstation_space_add { spaceSlug, shareSlug: A, section: "Deck" }
5. pitchstation_space_add { spaceSlug, shareSlug: B, section: "Financials" }
→ give the user ONE space URL (+ password)
```

Notes: documents stay live inside a space — `pitchstation_replace` on a member share updates it everywhere. Recipient-mode shares cannot join a space (their links are per-person). Removing a doc or revoking the space instantly kills space-based access; direct share links are unaffected.


## Expense packs — receipts → approvable report

`pitchstation_expense_create` starts an **expense pack** for a trip or project. The heavy lifting (uploading receipt photos, AI itemization into a table, editing rows) happens in the web app at `/expenses.html`; the tool is the entry point so an agent can kick one off.

- **`pitchstation_expense_create`** — `{ title, label? }` → pack id + a pointer to finish it in the app.

When the user says *"start an expense report for my Shanghai trip"* or *"I need to submit these receipts for reimbursement"*, create the pack, then tell them to drop their receipt photos (iPhone HEIC works) into the Expenses page and run **AI itemize** — the finished report publishes as one link their approver taps to Approve or Request changes. Requires the `create.expense` capability on their account (an admin grants it).

## Failure modes

- **`PITCHSTATION_TOKEN env var is required`** — create a PAT at `/tokens.html` and add it to the MCP server's env.
- **HTTP 401** — the PAT is missing or invalid.
- **HTTP 403 "API token revoked"** — re-create a PAT.
- **HTTP 413** — payload over the 10 MB cap. Strip embedded base64 images first, or upload as PDF.
- **`pdfBase64 is not a valid PDF`** — the input doesn't start with `%PDF-`. Don't base64 an HTML file by accident.
- **`aclUsernames only applies to account-mode shares`** — you set `aclUsernames` with `access: "link"`. Drop one or the other.

## Don't

- **Updating a document you already shared? Use `pitchstation_replace`, NOT a second `pitchstation_publish`.** Re-publishing mints a new URL and splits the document's analytics across two links. `pitchstation_replace` keeps the same URL, password, and analytics. Find the slug with `pitchstation_list`, then call `pitchstation_replace` with `slug` + the new `htmlContent`/`pdfBase64`. Use `mode: "announce"` with a short `note` (e.g. "Revised pricing on slide 8") when returning viewers should be told it changed; omit `mode` for a silent fix. Only `pitchstation_revoke` when the link should genuinely die.
- Don't echo the password back in chat unless the user asked — it's in the publish result for the user to copy.
- Don't pass `aclUsernames` with `access: "link"` or `access: "recipient"` (server returns 400).
