@typecaast/mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # @typecaast/mcp
2
+
3
+ A [Model Context Protocol](https://modelcontextprotocol.io) server for authoring
4
+ and validating [Typecaast](https://typecaast.com) configs **from your own
5
+ editor/project** — no need to open the playground.
6
+
7
+ It's the loop-closer for assembling a config by hand (or from a screenshot): your
8
+ assistant drafts the JSON guided by the schema + authoring guides this server
9
+ exposes, then calls `validate_config` to catch mistakes and fix them.
10
+
11
+ Rendering to video is **not** part of this server (it needs a headless browser) —
12
+ use [`@typecaast/cli`](https://www.npmjs.com/package/@typecaast/cli) for that.
13
+
14
+ ## Tools
15
+
16
+ | Tool | What it does |
17
+ | ----------------- | --------------------------------------------------------------------------- |
18
+ | `validate_config` | Validate a config (object or JSON string) → `{ valid, diagnostics }`. |
19
+ | `get_json_schema` | The config JSON Schema (draft-07). |
20
+ | `list_skins` | Built-in skins for `meta.skin.id` (id, name, themes, summary). |
21
+ | `scaffold_config` | A minimal valid starter config for a given skin id. |
22
+ | `get_docs` | Read an authoring guide (`authoring-configs`, `pacing`, `message-content`). |
23
+
24
+ The authoring guides are also exposed as MCP **resources** (`typecaast://docs/*`)
25
+ for clients that surface them.
26
+
27
+ ## Use it with Claude Code
28
+
29
+ ```bash
30
+ claude mcp add typecaast -- npx -y @typecaast/mcp
31
+ ```
32
+
33
+ ## Use it with Claude Desktop (or any MCP client)
34
+
35
+ Add to your client's MCP config (e.g. `claude_desktop_config.json`):
36
+
37
+ ```json
38
+ {
39
+ "mcpServers": {
40
+ "typecaast": {
41
+ "command": "npx",
42
+ "args": ["-y", "@typecaast/mcp"]
43
+ }
44
+ }
45
+ }
46
+ ```
47
+
48
+ The server speaks MCP over stdio. The binary is `typecaast-mcp` if you prefer to
49
+ install it globally (`npm i -g @typecaast/mcp`).
50
+
51
+ ## Typical flow
52
+
53
+ 1. Give your assistant a screenshot (or describe the conversation).
54
+ 2. It calls `list_skins` / `get_docs` to learn the format, drafts the JSON config.
55
+ 3. It calls `validate_config`; if there are `E_*` errors, it fixes them and re-validates.
56
+ 4. Drop the config into [`<Typecaast>`](https://www.npmjs.com/package/@typecaast/react)
57
+ or render it with `npx @typecaast/cli render config.json`.
58
+
59
+ ## License
60
+
61
+ Apache-2.0. Part of the [Typecaast](https://github.com/corywatilo/typecaast) monorepo.
package/dist/index.js ADDED
@@ -0,0 +1,346 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+
6
+ // src/server.ts
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { z } from "zod";
9
+
10
+ // package.json
11
+ var package_default = {
12
+ name: "@typecaast/mcp",
13
+ version: "0.1.0",
14
+ description: "Model Context Protocol server for authoring & validating Typecaast configs.",
15
+ license: "Apache-2.0",
16
+ repository: {
17
+ type: "git",
18
+ url: "git+https://github.com/corywatilo/typecaast.git",
19
+ directory: "packages/mcp"
20
+ },
21
+ type: "module",
22
+ bin: {
23
+ "typecaast-mcp": "./dist/index.js"
24
+ },
25
+ main: "./dist/index.js",
26
+ files: [
27
+ "dist"
28
+ ],
29
+ scripts: {
30
+ build: "tsup",
31
+ dev: "tsup --watch",
32
+ typecheck: "tsc --noEmit",
33
+ lint: "eslint src",
34
+ test: "vitest run",
35
+ clean: "rm -rf dist .turbo"
36
+ },
37
+ dependencies: {
38
+ "@modelcontextprotocol/sdk": "^1.29.0",
39
+ "@typecaast/schema": "workspace:*",
40
+ zod: "^4.4.3"
41
+ }
42
+ };
43
+
44
+ // ../../docs/authoring-configs.md
45
+ var authoring_configs_default = '# Authoring configs by hand\n\nA Typecaast simulation is **one JSON config**. The visual playground\n(<https://typecaast.com/playground>) is the easy way to build one, but the config\nis plain JSON \u2014 you can write or edit it by hand in any editor (or have an LLM do\nit). This is the reference for doing that without the playground.\n\n> **Timing lives in its own guide.** "Why are my messages too fast / too close\n> together?" and "how do I get ~1\u20132s between messages?" are answered in\n> [pacing.md](./pacing.md). Read that one for anything about gaps, delays, and\n> typing speed.\n\n## The shape\n\n```json\n{\n "$schema": "https://typecaast.com/schema/v1/typecaast.schema.json",\n "version": 1,\n "meta": {\n "canvas": { "width": 880, "height": 720 },\n "skin": { "id": "slack" }\n },\n "participants": [\n { "id": "me", "name": "You", "isSelf": true },\n { "id": "sam", "name": "Sam" }\n ],\n "pacing": { "readingWpm": 240 },\n "timeline": [\n { "type": "message", "from": "sam", "text": "ship it?" },\n { "type": "message", "from": "me", "text": "shipping \u{1F680}" }\n ]\n}\n```\n\nThat\'s a complete, valid config. Five top-level keys:\n\n| Key | Required | What it is |\n| -------------- | -------- | --------------------------------------------------------------------- |\n| `version` | yes | Config schema version. Currently `1` (a literal). |\n| `meta` | yes | Canvas, skin, theme, fps \u2014 how it renders. |\n| `participants` | yes | Who is in the conversation. |\n| `pacing` | no | Global timing model. Omit for defaults. See [pacing.md](./pacing.md). |\n| `timeline` | yes | The ordered list of steps that play out. |\n\nAdd the `$schema` line at the top and your editor gives you autocomplete and\ninline validation against the live schema. It\'s optional but recommended.\n\n## `meta`\n\n```json\n"meta": {\n "canvas": { "width": 880, "height": 720 },\n "skin": { "id": "slack", "options": { "channel": "#alerts" } },\n "fps": 30,\n "fit": "reflow",\n "theme": "auto",\n "seed": 42,\n "background": "transparent",\n "assets": "inline",\n "composer": "auto",\n "loop": false\n}\n```\n\n| Field | Default | Notes |\n| ------------ | --------------- | ----------------------------------------------------------------------------------------------------- |\n| `canvas` | \u2014 | `{ width, height }` in px. Required. The authoring reference size / the video frame. |\n| `skin` | \u2014 | `{ id, options? }`. Required. `id` picks the skin (see [Skins](#skins)); `options` are skin-specific. |\n| `fps` | `30` | Frames per second for video export. |\n| `fit` | `"reflow"` | `reflow` (re-wrap to container), `scale` (CSS-scale the canvas), or `fixed` (pin to px). |\n| `theme` | `"auto"` | `light`, `dark`, or `auto` (follows the host page; video resolves `auto` \u2192 `light`). |\n| `seed` | `42` | Seeds all deterministic jitter \u2014 same seed \u21D2 identical timings. |\n| `background` | `"transparent"` | `"transparent"` or any CSS color. |\n| `assets` | `"inline"` | `inline` (embed images as data URLs) or `url` (reference hosted images). |\n| `composer` | `"auto"` | Reply box: `auto` (shown only while typing/sending), `always`, or `never`. |\n| `loop` | `false` | Auto-replay at the end (unless the `<Typecaast>` consumer passes an explicit `loop`). |\n\n## `participants`\n\nAn array of speakers. Reference them everywhere by `id`.\n\n```json\n"participants": [\n { "id": "me", "name": "You", "isSelf": true },\n { "id": "sam", "name": "Sam", "avatar": "https://\u2026/sam.png", "color": "#5b3a8e" },\n { "id": "bot", "name": "PostHog", "kind": "app" }\n]\n```\n\n| Field | Required | Notes |\n| -------- | -------- | -------------------------------------------------------------------------------- |\n| `id` | yes | Stable id used by `from` / `target` / `by` in the timeline. |\n| `name` | yes | Display name. |\n| `avatar` | no | Data URL, or a hosted URL (per `meta.assets`). |\n| `color` | no | Accent color (CSS) some skins use for the author. |\n| `isSelf` | no | Marks the viewer \u2014 rendered on the "self" side and as the composer\'s author. |\n| `kind` | no | `"person"` (default) or `"app"` (a bot/integration; skins render it distinctly). |\n\nSet `isSelf: true` on exactly one participant if you use the composer\n(`composerType` / `send`).\n\n## `timeline` \u2014 the steps\n\nThe timeline is an ordered array. Each step has a `type` and its own fields. Two\nfields are shared by **every** step:\n\n- `id` \u2014 optional. Give a step an id so a later `reaction`, `edit`, `delete`, or\n `readReceipt` can target it.\n- `instant` \u2014 optional `true` to reveal with no animation and **no computed\n pacing gap** (handy when you want to control spacing yourself \u2014 see\n [pacing.md](./pacing.md)).\n\nSteps that target an earlier message take a `target`: either a message\'s `id` or\nthe literal `"$prev"` (the most-recent message). `target` defaults to `"$prev"`.\n\n### The step types\n\n| `type` | Purpose |\n| -------------- | ------------------------------------------------------------------------ |\n| `message` | An incoming message (optionally preceded by a typing indicator). |\n| `typing` | A standalone typing indicator (no message need follow). |\n| `composerType` | The `isSelf` participant typing into the composer, char by char. |\n| `send` | Commit the composer\'s current text to the thread. |\n| `reaction` | An emoji reaction landing on a target message. |\n| `edit` | Replace a previously sent message\'s body. |\n| `delete` | Remove a previously sent message. |\n| `readReceipt` | A read-receipt indicator (skins that support it). |\n| `system` | A system / notice line (e.g. "Sam joined #alerts") \u2014 not a chat message. |\n| `delay` | An explicit pause on the timeline. See [pacing.md](./pacing.md). |\n\n### `message`\n\n```json\n{ "type": "message", "from": "sam", "text": "on it" }\n```\n\n| Field | Notes |\n| --------- | ----------------------------------------------------------------------------------------------------- |\n| `from` | Participant id. Required. |\n| `text` | Message body. Supports Slack-style mrkdwn \u2014 see [message-content.md](./message-content.md). |\n| `images` | `[{ src, alt?, width?, height? }]` \u2014 in-message images. |\n| `content` | Explicit Block Kit content nodes (wins over `text`) \u2014 see [message-content.md](./message-content.md). |\n| `typing` | Show a typing indicator first: `true`, or `{ "showTypingFor": 1800 }` (ms). |\n\nModel an app "card" as a `message` from an `app` participant carrying `content`\n(header/section/context/actions/\u2026), **not** a `system` step.\n\n### `typing`\n\n```json\n{ "type": "typing", "from": "sam", "showTypingFor": 1500 }\n```\n\n`showTypingFor` is in **milliseconds** (defaults to ~1500ms if omitted).\n\n### `composerType` and `send`\n\n```json\n{ "type": "composerType", "from": "me", "text": "let me check\u2026" },\n{ "type": "send" }\n```\n\n`composerType` types into the composer (the `from` should be your `isSelf`\nparticipant); `send` then posts it. `send.from` is optional \u2014 it inherits the\ncomposer\'s author. `composerType.typingDuration` (ms) overrides the computed\ntyping time.\n\n### `reaction`\n\n```json\n{ "type": "reaction", "target": "$prev", "emoji": "\u{1F680}", "delay": 800 }\n```\n\n| Field | Notes |\n| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `emoji` | The emoji. Required. |\n| `target` | Message id or `"$prev"` (default). |\n| `from` | Optional reactor. |\n| `shortcode` | Optional shortcode without colons (e.g. `"rocket"`) \u2014 shown in some skins\' tooltips. |\n| `delay` | Gap (ms) from when the target appears before the reaction lands. **This `delay` is a field on the reaction, not the `delay` step type** \u2014 don\'t confuse them (see [pacing.md](./pacing.md)). |\n\n### `edit` and `delete`\n\n```json\n{ "type": "edit", "target": "m2", "text": "fixed typo" },\n{ "type": "delete", "target": "m3" }\n```\n\nBoth default `target` to `"$prev"`. `edit` takes the same body fields as\n`message` (`text` / `images` / `content`).\n\n### `readReceipt`\n\n```json\n{ "type": "readReceipt", "by": "sam", "target": "m2" }\n```\n\n`by` (who read it) and `target` (up to which message) are both optional.\n\n### `system`\n\n```json\n{ "type": "system", "text": "Sam joined #alerts" }\n```\n\nA centered notice / tool-output line, rendered distinctly per skin. Takes the\nsame body fields as `message`.\n\n### `delay`\n\n```json\n{ "type": "delay", "duration": 1500 }\n```\n\nAn explicit pause. `duration` is in **milliseconds** and is **required**. This is\nthe most predictable way to space messages out \u2014 see [pacing.md](./pacing.md).\n\n## Skins\n\n`meta.skin.id` selects the skin. The conversation content is the same regardless\nof skin; the skin decides how it looks. Built-in ids:\n\n`slack` \xB7 `telegram` \xB7 `claude-code` \xB7 `imessage` \xB7 `messages-macos` \xB7\n`whatsapp` \xB7 `cursor` \xB7 `discord`\n\nSome steps aren\'t rendered by every skin (e.g. a terminal skin has no\nreactions). The skin\'s capabilities decide; unsupported steps are dropped. Pick a\nskin that supports the steps you use.\n\n## Validate it\n\nAlways validate after editing by hand:\n\n- **CLI:** `npx @typecaast/cli validate my-config.json` (exit 0 = OK; prints\n `E_*` errors / `W_*` warnings with locations and hints \u2014 see\n [errors.md](./errors.md)).\n- **MCP:** if you\'ve added the [`@typecaast/mcp`](../packages/mcp/README.md)\n server to your editor, call its `validate_config` tool. This is the easy loop\n when drafting a config (e.g. from a screenshot) in your own project.\n\n## See also\n\n- [pacing.md](./pacing.md) \u2014 timing, delays, and the "1\u20132s between messages" recipes.\n- [message-content.md](./message-content.md) \u2014 message bodies: mrkdwn + Block Kit.\n- The JSON Schema: <https://typecaast.com/schema/v1/typecaast.schema.json>.\n- Machine-readable index for LLMs: <https://typecaast.com/llms.txt>.\n </content>\n';
46
+
47
+ // ../../docs/pacing.md
48
+ var pacing_default = '# Pacing & timing\n\nTypecaast **auto-paces** a conversation so you don\'t have to time every step by\nhand: it derives the gap before each message from how long the previous message\ntakes to "read", and how long typing takes from the text length. This is why a\nconfig with no timing fields still plays at a human rhythm.\n\nIt\'s also the #1 source of "why is this too fast / why are my messages bunched\nup?" \u2014 including the common ask, **"I want ~1\u20132s between messages."** This guide\nexplains the model and the levers. (For the full step reference, see\n[authoring-configs.md](./authoring-configs.md).)\n\n## The model\n\nTiming is controlled by the optional top-level `pacing` object. Omit it and you\nget the full default model:\n\n```json\n"pacing": {\n "readingWpm": 240,\n "typingCps": 14,\n "humanize": 0.15,\n "startDelayMs": 400\n}\n```\n\n| Field | Default | Unit | What it controls |\n| -------------- | ------- | --------- | ------------------------------------------------------------------------------- |\n| `readingWpm` | `240` | words/min | The gap **before an incoming message** \u2248 the time to read the **previous** one. |\n| `typingCps` | `14` | chars/sec | Composer typing speed, and how long a typing indicator shows before a message. |\n| `humanize` | `0.15` | fraction | \xB115% **seeded** jitter on computed gaps/durations so it doesn\'t feel robotic. |\n| `startDelayMs` | `400` | ms | Delay before the very first event. |\n\nEvery value is overridable per step in the timeline; per-step values win over the\ncomputed ones.\n\n### How the gap is computed\n\nThe key thing to understand: **the gap before a message scales with the length of\nthe _previous_ message**, because it models the other person reading it before\nreplying. Roughly:\n\n```\nwords \u2248 characters / 5\ngap(ms) \u2248 (words / readingWpm) \xD7 60000 (then \xB1humanize jitter)\n```\n\nSo at the default 240 wpm:\n\n- A short line like `"ship it?"` (~1.6 words) \u2192 gap \u2248 **400ms**.\n- A sentence of ~40 characters (~8 words) \u2192 gap \u2248 **2000ms**.\n\n**This is the gotcha:** with short messages, the default gaps are short (a few\nhundred ms), so the conversation feels snappy/bunched. Lowering `readingWpm` or\nadding explicit `delay` steps is how you stretch it out.\n\n### Other automatic durations\n\n- Typing indicator before a message (`message.typing: true`) lasts about as long\n as it would take to type that message at `typingCps`. A standalone `typing`\n step with no `showTypingFor` defaults to ~1500ms.\n- A `reaction` with no `delay` lands ~1000ms after its target appears.\n- `instant: true` on a step skips the reveal animation **and** the computed gap.\n\n### Determinism\n\nAll jitter is seeded from `meta.seed` (default `42`). The same `(config, seed)`\nalways produces identical timings \u2014 there is no `Math.random()`. Change `seed`\nto get a different (but still repeatable) jitter pattern.\n\n## "I want ~1\u20132 seconds between messages"\n\nFour ways, most predictable first.\n\n### 1. Insert a `delay` step (recommended \u2014 exact and explicit)\n\nA `delay` step is a fixed pause whose `duration` is in **milliseconds**:\n\n```json\n"timeline": [\n { "type": "message", "from": "sam", "text": "ship it?" },\n { "type": "delay", "duration": 1500 },\n { "type": "message", "from": "me", "text": "shipping \u{1F680}" }\n]\n```\n\nThat guarantees ~1.5s between those two messages regardless of their length. Use\nit between each pair of messages you want spaced out. It\'s the most reliable knob\nbecause it doesn\'t depend on text length.\n\n> **Don\'t confuse the two `delay`s.** The `delay` **step type** (`{ "type":\n"delay", "duration": \u2026 }`) is a standalone pause. A `reaction` step also has a\n> `delay` **field** \u2014 that one is the gap before the _reaction_ lands on its\n> target. Different things.\n\n### 2. Lower `readingWpm` globally\n\nSlowing the "reading speed" lengthens every auto-computed gap at once:\n\n```json\n"pacing": { "readingWpm": 80 }\n```\n\nAt ~80 wpm a short `"ship it?"` reply gaps ~1.2s; longer messages gap\nproportionally more. Good for a uniformly slower feel. **Caveat:** because the\ngap still scales with text length, very short messages stay shorter than very\nlong ones \u2014 if you need a precise gap, use a `delay` step (#1).\n\nRough guide for a short one-liner: ~120 wpm \u2248 0.8s, ~80 wpm \u2248 1.2s, ~60 wpm \u2248\n1.6s.\n\n### 3. `instant` + `delay` for full manual control\n\nDrop the automatic gaps entirely and place every pause yourself:\n\n```json\n{ "type": "message", "from": "sam", "text": "ship it?", "instant": true },\n{ "type": "delay", "duration": 1200 },\n{ "type": "message", "from": "me", "text": "shipping \u{1F680}", "instant": true }\n```\n\n`instant` removes a step\'s computed gap and reveal animation, so the only spacing\nis your `delay` steps. Use this when you want a precisely choreographed timeline.\n\n### 4. Pad with a typing indicator\n\nA typing indicator before a message both adds time and reads naturally:\n\n```json\n{ "type": "typing", "from": "sam", "showTypingFor": 1500 },\n{ "type": "message", "from": "sam", "text": "shipping \u{1F680}" }\n```\n\nor inline on the message:\n\n```json\n{\n "type": "message",\n "from": "sam",\n "text": "shipping \u{1F680}",\n "typing": { "showTypingFor": 1500 }\n}\n```\n\n`showTypingFor` is in **milliseconds**.\n\n## Worked example\n\nTwo short messages with a deliberate 1.5s gap:\n\n```json\n{\n "$schema": "https://typecaast.com/schema/v1/typecaast.schema.json",\n "version": 1,\n "meta": {\n "canvas": { "width": 600, "height": 400 },\n "skin": { "id": "imessage" }\n },\n "participants": [\n { "id": "me", "name": "You", "isSelf": true },\n { "id": "sam", "name": "Sam" }\n ],\n "timeline": [\n { "type": "message", "from": "sam", "text": "ship it?" },\n { "type": "delay", "duration": 1500 },\n { "type": "message", "from": "me", "text": "shipping \u{1F680}" }\n ]\n}\n```\n\nTimeline: first message reveals at ~`startDelayMs` (400ms); the `delay` holds\n1500ms; the reply reveals ~1.5s later. Without the `delay`, that gap would be the\ndefault ~400ms reading time of `"ship it?"`.\n\n## See also\n\n- [authoring-configs.md](./authoring-configs.md) \u2014 the full config + step reference.\n- [message-content.md](./message-content.md) \u2014 message bodies (mrkdwn + Block Kit).\n </content>\n';
49
+
50
+ // ../../docs/message-content.md
51
+ var message_content_default = '# Authoring message content\n\nA message\'s body is authored in its `content`. The simplest form is a plain\n`text` string; richer messages (especially **app messages**) use **Block Kit**\ncontent nodes \u2014 the same primitives a real Slack app emits.\n\n> The easiest way to author this is the [playground](https://typecaast.com/playground)\n> \u2014 it has a typed block editor. This doc is the reference for the JSON it\n> produces. Point your editor at the bundled JSON Schema\n> (`"$schema": ".../typecaast.schema.json"`) for autocomplete.\n\n## Plain text + mrkdwn\n\nMost messages just need `text`. It\'s parsed for inline marks:\n\n```json\n{ "type": "message", "from": "cory", "text": "shipped *it* \u2014 see `deploy.ts`" }\n```\n\n| Syntax | Renders | Inline node |\n| ---------------------- | ----------------------------------------------------- | ------------ |\n| `*bold*` | **bold** | `bold` |\n| `_italic_` | _italic_ | `italic` |\n| `~strike~` | ~~strike~~ | `strike` |\n| `` `code` `` | `code` | `code` |\n| `https://\u2026` | a link | `link` |\n| `@name` | a mention pill | `mention` |\n| `<@id>` | a mention resolved to that participant\'s display name | `mention` |\n| emoji (`\u{1F7E0}`, `:tada:`) | the glyph | left in text |\n\n`<@id>` is the robust way to mention someone whose display name has spaces\n(`<@joe>` \u2192 "@Joe Saunderson"); it resolves against `participants` at compile.\n\nIn-message images use the `images` sugar:\n\n```json\n{\n "type": "message",\n "from": "cory",\n "text": "here\'s the toast:",\n "images": [{ "src": "./toast.png", "alt": "error toast", "width": 320 }]\n}\n```\n\n## App messages (Block Kit)\n\nAn **app message** is a normal `message` from a participant with\n`"kind": "app"` \u2014 the sender drives the "APP" badge \u2014 whose `content` is a list\nof Block Kit nodes. **Slack renders the full set; other skins show the text and\nskip the rest** (a Slack-targeted config degrades gracefully elsewhere).\n\nBlocks carry a `text` string (parsed like above) or pre-resolved `spans`:\n\n```json\n{\n "type": "message",\n "from": "posthog",\n "content": [\n { "type": "header", "text": "perf(inbox): Fix 26s admin inbox load" },\n {\n "type": "context",\n "elements": [\n {\n "type": "text",\n "text": "\u{1F7E0} *P2* \xB7 Session replay \xB7 rvenvy/rvenvy-ai"\n }\n ]\n },\n { "type": "section", "text": "Sales reps hit prolonged loading spinners\u2026" },\n {\n "type": "context",\n "elements": [\n {\n "type": "text",\n "text": "2 signals \xB7 Suggested reviewers: <@joe> <@cory>"\n }\n ]\n },\n { "type": "divider" },\n {\n "type": "actions",\n "elements": [\n { "type": "button", "label": "Review PR", "href": "https://\u2026" },\n { "type": "button", "label": "Open in PostHog" },\n { "type": "button", "label": "Dismiss", "style": "danger" }\n ]\n }\n ]\n}\n```\n\n### Block types\n\n| Node | Shape | Notes |\n| ------------ | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `header` | `{ text }` | Large bold heading (plain text). |\n| `section` | `{ text \\| spans, accessory?, fields? }` | A paragraph. `accessory` is a `button` or `image` to its right; `fields` is a 2-column grid of `{ text \\| spans }`. |\n| `context` | `{ elements: [...] }` | Small muted row; each element is `{ type:"text", text }` or `{ type:"image", src, alt? }`. |\n| `divider` | `{}` | A horizontal rule. |\n| `actions` | `{ elements: [button\u2026] }` | A row of buttons. |\n| `image` | `{ src, alt?, width?, height? }` | A standalone image. |\n| `codeblock` | `{ text, lang? }` | A monospaced preformatted box (Slack\'s fenced ` ``` ` block). `text` is **literal** \u2014 whitespace and newlines are kept and it is **not** parsed for marks. |\n| `attachment` | `{ color?, content: [...] }` | Nested blocks behind a colored left bar (the legacy "attachment" look). `color` is any CSS color. |\n\n### Buttons\n\nA `button` element appears in an `actions` block or as a `section` accessory:\n\n```json\n{\n "type": "button",\n "label": "View PR",\n "href": "https://\u2026",\n "style": "primary"\n}\n```\n\n- `style`: `"primary"` (filled green), `"danger"` (red outline), or omit for the\n default outlined button.\n- `href`: with one, the button is a real link (opens in a new tab); without one\n it renders inert.\n\n### Code blocks\n\nFor a table, log, or snippet, use a `codeblock` \u2014 Slack\'s fenced ` ``` `\nblock, a monospaced box that preserves whitespace and newlines verbatim. Unlike\nthe inline `` `code` `` mark (a single-line pill inside a sentence), a `codeblock`\nis its own block and its `text` is **literal**: `*`, `~`, `<@id>` etc. are shown\nas typed, never parsed. Author the canvas wide enough that the widest line fits on\none line, then scale the embed down on the page (see below).\n\n```json\n{\n "type": "message",\n "from": "posthog",\n "content": [\n {\n "type": "codeblock",\n "text": "Metric A B\\nReturned 64% 96%\\nActive weeks 2.9 7.2"\n }\n ]\n}\n```\n\nA wide monospace block won\'t reflow, so give the instance a wide\n`meta.canvas.width` and set `"fit": "scale"`; the host wraps it in a sized\ncontainer and the whole instance scales down to fit (the canvas keeps its internal\npixel width, so the table stays one line per row).\n\n### The colored bar (attachment)\n\nTo get the legacy left-bar card look, wrap blocks in an `attachment`:\n\n```json\n{\n "type": "message",\n "from": "posthog",\n "content": [\n {\n "type": "attachment",\n "color": "#36a64f",\n "content": [\n { "type": "section", "text": "Pull request opened." },\n {\n "type": "actions",\n "elements": [\n { "type": "button", "label": "View PR", "style": "primary" }\n ]\n }\n ]\n }\n ]\n}\n```\n\n## System / notice lines\n\nThe `system` step is **not** an app card \u2014 it\'s a non-message notice line\n("X joined #channel", an agent\'s tool-output line), rendered distinctly per skin\n(centered/muted in Slack, iMessage, WhatsApp; an agent line in Cursor/Claude\nCode). It carries only `from?` + text/`content`:\n\n```json\n{ "type": "system", "from": "posthog", "text": "Cory joined #signals" }\n```\n\n## The reply box\n\nThe composer ("reply box") visibility is set by `meta.composer`: `"auto"`\n(default \u2014 shown only while someone is composing), `"always"` (always shown,\nidle when nobody is typing \u2014 no `isSelf` participant required), or `"never"`.\n';
52
+
53
+ // src/docs.ts
54
+ var DOCS = [
55
+ {
56
+ slug: "authoring-configs",
57
+ title: "Authoring configs by hand",
58
+ text: authoring_configs_default
59
+ },
60
+ { slug: "pacing", title: "Pacing & timing", text: pacing_default },
61
+ {
62
+ slug: "message-content",
63
+ title: "Message content",
64
+ text: message_content_default
65
+ }
66
+ ];
67
+
68
+ // src/core.ts
69
+ import {
70
+ validateConfig,
71
+ configJsonSchema
72
+ } from "@typecaast/schema";
73
+
74
+ // ../../registry/skins.json
75
+ var skins_default = {
76
+ $schema: "./skins.schema.json",
77
+ version: 1,
78
+ skins: [
79
+ {
80
+ id: "slack",
81
+ name: "Slack",
82
+ official: true,
83
+ author: "Typecaast",
84
+ package: "@typecaast/skins",
85
+ export: "slack",
86
+ themes: ["light", "dark"],
87
+ intendedFonts: ["Lato"],
88
+ summary: "Slack-style thread: app cards, reactions, replies.",
89
+ verified: "2026-Q2"
90
+ },
91
+ {
92
+ id: "telegram",
93
+ name: "Telegram",
94
+ official: true,
95
+ author: "Typecaast",
96
+ package: "@typecaast/skins",
97
+ export: "telegram",
98
+ themes: ["light", "dark"],
99
+ intendedFonts: ["Roboto"],
100
+ summary: "Telegram chat: bubbles with tails, reactions, bot inline buttons.",
101
+ verified: "2026-Q2"
102
+ },
103
+ {
104
+ id: "claude-code",
105
+ name: "Claude Code (TUI)",
106
+ official: true,
107
+ author: "Typecaast",
108
+ package: "@typecaast/skins",
109
+ export: "claudeCode",
110
+ themes: ["dark"],
111
+ intendedFonts: ["JetBrains Mono"],
112
+ summary: "Terminal UI with streaming output and a spinner.",
113
+ verified: "2026-Q2"
114
+ },
115
+ {
116
+ id: "imessage",
117
+ name: "iMessage (iOS)",
118
+ official: true,
119
+ author: "Typecaast",
120
+ package: "@typecaast/skins",
121
+ export: "imessage",
122
+ themes: ["light", "dark"],
123
+ intendedFonts: ["SF Pro (\u2192 Inter)"],
124
+ summary: "iPhone Messages: bubbles, tapbacks, status bar, keyboard.",
125
+ verified: "2026-Q2"
126
+ },
127
+ {
128
+ id: "messages-macos",
129
+ name: "Messages (macOS)",
130
+ official: true,
131
+ author: "Typecaast",
132
+ package: "@typecaast/skins",
133
+ export: "messagesMacos",
134
+ themes: ["light", "dark"],
135
+ intendedFonts: ["SF Pro (\u2192 Inter)"],
136
+ summary: "Desktop Messages: window chrome + conversation sidebar.",
137
+ verified: "2026-Q2"
138
+ },
139
+ {
140
+ id: "whatsapp",
141
+ name: "WhatsApp",
142
+ official: true,
143
+ author: "Typecaast",
144
+ package: "@typecaast/skins",
145
+ export: "whatsapp",
146
+ themes: ["light", "dark"],
147
+ intendedFonts: ["Helvetica Neue (system)"],
148
+ summary: "WhatsApp: in-bubble timestamps + double-tick receipts.",
149
+ verified: "2026-Q2"
150
+ },
151
+ {
152
+ id: "cursor",
153
+ name: "Cursor panel",
154
+ official: true,
155
+ author: "Typecaast",
156
+ package: "@typecaast/skins",
157
+ export: "cursor",
158
+ themes: ["dark", "light"],
159
+ intendedFonts: ["gg sans (system)"],
160
+ summary: "Cursor AI side-panel: prompts, responses, model chips.",
161
+ verified: "2026-Q2"
162
+ },
163
+ {
164
+ id: "discord",
165
+ name: "Discord",
166
+ official: true,
167
+ author: "Typecaast",
168
+ package: "@typecaast/skins",
169
+ export: "discord",
170
+ themes: ["dark"],
171
+ intendedFonts: ["gg sans (\u2192 Noto Sans)"],
172
+ summary: "Discord channel: role colors, grouped messages, reactions.",
173
+ verified: "2026-Q2"
174
+ }
175
+ ]
176
+ };
177
+
178
+ // src/core.ts
179
+ var SCHEMA_ID = "https://typecaast.com/schema/v1/typecaast.schema.json";
180
+ var SKINS = skins_default.skins.filter((s) => s.official).map((s) => ({
181
+ id: s.id,
182
+ name: s.name,
183
+ themes: s.themes,
184
+ summary: s.summary
185
+ }));
186
+ function jsonSchema() {
187
+ return {
188
+ $schema: "http://json-schema.org/draft-07/schema#",
189
+ $id: SCHEMA_ID,
190
+ title: "Typecaast config",
191
+ ...configJsonSchema()
192
+ };
193
+ }
194
+ function validate(config) {
195
+ let value = config;
196
+ if (typeof config === "string") {
197
+ try {
198
+ value = JSON.parse(config);
199
+ } catch (e) {
200
+ return {
201
+ valid: false,
202
+ diagnostics: [],
203
+ error: `Invalid JSON: ${e.message}`
204
+ };
205
+ }
206
+ }
207
+ const diagnostics = validateConfig(value);
208
+ const valid = !diagnostics.some((d) => d.severity === "error");
209
+ return { valid, diagnostics };
210
+ }
211
+ function scaffoldConfig(skinId = "slack") {
212
+ return {
213
+ $schema: SCHEMA_ID,
214
+ version: 1,
215
+ meta: { canvas: { width: 600, height: 760 }, skin: { id: skinId } },
216
+ participants: [
217
+ { id: "me", name: "You", isSelf: true },
218
+ { id: "sam", name: "Sam" }
219
+ ],
220
+ timeline: [
221
+ { type: "message", from: "sam", text: "ship it?" },
222
+ { type: "delay", duration: 1500 },
223
+ { type: "message", from: "me", text: "shipping \u{1F680}" }
224
+ ]
225
+ };
226
+ }
227
+
228
+ // src/server.ts
229
+ function jsonText(value) {
230
+ return {
231
+ content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
232
+ };
233
+ }
234
+ function createServer() {
235
+ const server = new McpServer({ name: "typecaast", version: package_default.version });
236
+ server.registerTool(
237
+ "validate_config",
238
+ {
239
+ title: "Validate a Typecaast config",
240
+ description: "Validate a Typecaast config (object or JSON string) against the schema and semantic checks. Returns { valid, diagnostics }. Run this after authoring or editing a config (e.g. one drafted from a screenshot).",
241
+ inputSchema: {
242
+ config: z.union([z.string(), z.record(z.string(), z.unknown())]).describe("The config, as a JSON string or an object.")
243
+ }
244
+ },
245
+ ({ config }) => jsonText(validate(config))
246
+ );
247
+ server.registerTool(
248
+ "get_json_schema",
249
+ {
250
+ title: "Get the config JSON Schema",
251
+ description: 'Return the Typecaast config JSON Schema (draft-07). Reference it from a config via a "$schema" line, or use it to build a config skeleton.',
252
+ inputSchema: {}
253
+ },
254
+ () => jsonText(jsonSchema())
255
+ );
256
+ server.registerTool(
257
+ "list_skins",
258
+ {
259
+ title: "List built-in skins",
260
+ description: "List the built-in skins (set one as meta.skin.id), with display name, supported themes, and a one-line summary.",
261
+ inputSchema: {}
262
+ },
263
+ () => jsonText(SKINS)
264
+ );
265
+ server.registerTool(
266
+ "scaffold_config",
267
+ {
268
+ title: "Scaffold a starter config",
269
+ description: 'Return a minimal valid Typecaast config for the given skin id (default "slack") \u2014 a starting point to edit.',
270
+ inputSchema: {
271
+ skinId: z.string().optional().describe('Skin id, e.g. "slack" (default), "imessage", "discord".')
272
+ }
273
+ },
274
+ ({ skinId }) => jsonText(scaffoldConfig(skinId))
275
+ );
276
+ server.registerTool(
277
+ "get_docs",
278
+ {
279
+ title: "Read an authoring guide",
280
+ description: `Return a Typecaast authoring guide as markdown. Slugs: ${DOCS.map(
281
+ (d) => d.slug
282
+ ).join(", ")}. Omit "slug" to list them.`,
283
+ inputSchema: {
284
+ slug: z.string().optional().describe(
285
+ 'Guide slug, e.g. "pacing". Omit to list available guides.'
286
+ )
287
+ }
288
+ },
289
+ ({ slug }) => {
290
+ if (!slug) {
291
+ return {
292
+ content: [
293
+ {
294
+ type: "text",
295
+ text: DOCS.map((d) => `- ${d.slug}: ${d.title}`).join("\n")
296
+ }
297
+ ]
298
+ };
299
+ }
300
+ const doc = DOCS.find((d) => d.slug === slug);
301
+ if (!doc) {
302
+ return {
303
+ content: [
304
+ {
305
+ type: "text",
306
+ text: `Unknown guide "${slug}". Available: ${DOCS.map(
307
+ (d) => d.slug
308
+ ).join(", ")}.`
309
+ }
310
+ ],
311
+ isError: true
312
+ };
313
+ }
314
+ return { content: [{ type: "text", text: doc.text }] };
315
+ }
316
+ );
317
+ for (const doc of DOCS) {
318
+ server.registerResource(
319
+ `docs-${doc.slug}`,
320
+ `typecaast://docs/${doc.slug}`,
321
+ {
322
+ title: doc.title,
323
+ description: `Typecaast authoring guide: ${doc.title}`,
324
+ mimeType: "text/markdown"
325
+ },
326
+ (uri) => ({
327
+ contents: [
328
+ { uri: uri.href, mimeType: "text/markdown", text: doc.text }
329
+ ]
330
+ })
331
+ );
332
+ }
333
+ return server;
334
+ }
335
+
336
+ // src/index.ts
337
+ async function main() {
338
+ const server = createServer();
339
+ const transport = new StdioServerTransport();
340
+ await server.connect(transport);
341
+ }
342
+ main().catch((err) => {
343
+ console.error("typecaast-mcp failed to start:", err);
344
+ process.exit(1);
345
+ });
346
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/server.ts","../package.json","../../../docs/authoring-configs.md","../../../docs/pacing.md","../../../docs/message-content.md","../src/docs.ts","../src/core.ts","../../../registry/skins.json"],"sourcesContent":["import { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { createServer } from \"./server.js\";\n\nasync function main(): Promise<void> {\n const server = createServer();\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n\nmain().catch((err: unknown) => {\n console.error(\"typecaast-mcp failed to start:\", err);\n process.exit(1);\n});\n","import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport pkg from \"../package.json\";\nimport { DOCS } from \"./docs.js\";\nimport { SKINS, jsonSchema, scaffoldConfig, validate } from \"./core.js\";\n\n/** A tool result that returns a value as pretty-printed JSON text. */\nfunction jsonText(value: unknown): {\n content: { type: \"text\"; text: string }[];\n} {\n return {\n content: [{ type: \"text\", text: JSON.stringify(value, null, 2) }],\n };\n}\n\n/** Build the Typecaast MCP server with all tools + doc resources registered. */\nexport function createServer(): McpServer {\n const server = new McpServer({ name: \"typecaast\", version: pkg.version });\n\n server.registerTool(\n \"validate_config\",\n {\n title: \"Validate a Typecaast config\",\n description:\n \"Validate a Typecaast config (object or JSON string) against the schema and semantic checks. Returns { valid, diagnostics }. Run this after authoring or editing a config (e.g. one drafted from a screenshot).\",\n inputSchema: {\n config: z\n .union([z.string(), z.record(z.string(), z.unknown())])\n .describe(\"The config, as a JSON string or an object.\"),\n },\n },\n ({ config }) => jsonText(validate(config)),\n );\n\n server.registerTool(\n \"get_json_schema\",\n {\n title: \"Get the config JSON Schema\",\n description:\n 'Return the Typecaast config JSON Schema (draft-07). Reference it from a config via a \"$schema\" line, or use it to build a config skeleton.',\n inputSchema: {},\n },\n () => jsonText(jsonSchema()),\n );\n\n server.registerTool(\n \"list_skins\",\n {\n title: \"List built-in skins\",\n description:\n \"List the built-in skins (set one as meta.skin.id), with display name, supported themes, and a one-line summary.\",\n inputSchema: {},\n },\n () => jsonText(SKINS),\n );\n\n server.registerTool(\n \"scaffold_config\",\n {\n title: \"Scaffold a starter config\",\n description:\n 'Return a minimal valid Typecaast config for the given skin id (default \"slack\") — a starting point to edit.',\n inputSchema: {\n skinId: z\n .string()\n .optional()\n .describe('Skin id, e.g. \"slack\" (default), \"imessage\", \"discord\".'),\n },\n },\n ({ skinId }) => jsonText(scaffoldConfig(skinId)),\n );\n\n server.registerTool(\n \"get_docs\",\n {\n title: \"Read an authoring guide\",\n description: `Return a Typecaast authoring guide as markdown. Slugs: ${DOCS.map(\n (d) => d.slug,\n ).join(\", \")}. Omit \"slug\" to list them.`,\n inputSchema: {\n slug: z\n .string()\n .optional()\n .describe(\n 'Guide slug, e.g. \"pacing\". Omit to list available guides.',\n ),\n },\n },\n ({ slug }) => {\n if (!slug) {\n return {\n content: [\n {\n type: \"text\",\n text: DOCS.map((d) => `- ${d.slug}: ${d.title}`).join(\"\\n\"),\n },\n ],\n };\n }\n const doc = DOCS.find((d) => d.slug === slug);\n if (!doc) {\n return {\n content: [\n {\n type: \"text\",\n text: `Unknown guide \"${slug}\". Available: ${DOCS.map(\n (d) => d.slug,\n ).join(\", \")}.`,\n },\n ],\n isError: true,\n };\n }\n return { content: [{ type: \"text\", text: doc.text }] };\n },\n );\n\n for (const doc of DOCS) {\n server.registerResource(\n `docs-${doc.slug}`,\n `typecaast://docs/${doc.slug}`,\n {\n title: doc.title,\n description: `Typecaast authoring guide: ${doc.title}`,\n mimeType: \"text/markdown\",\n },\n (uri) => ({\n contents: [\n { uri: uri.href, mimeType: \"text/markdown\", text: doc.text },\n ],\n }),\n );\n }\n\n return server;\n}\n","{\n \"name\": \"@typecaast/mcp\",\n \"version\": \"0.1.0\",\n \"description\": \"Model Context Protocol server for authoring & validating Typecaast configs.\",\n \"license\": \"Apache-2.0\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/corywatilo/typecaast.git\",\n \"directory\": \"packages/mcp\"\n },\n \"type\": \"module\",\n \"bin\": {\n \"typecaast-mcp\": \"./dist/index.js\"\n },\n \"main\": \"./dist/index.js\",\n \"files\": [\n \"dist\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"typecheck\": \"tsc --noEmit\",\n \"lint\": \"eslint src\",\n \"test\": \"vitest run\",\n \"clean\": \"rm -rf dist .turbo\"\n },\n \"dependencies\": {\n \"@modelcontextprotocol/sdk\": \"^1.29.0\",\n \"@typecaast/schema\": \"workspace:*\",\n \"zod\": \"^4.4.3\"\n }\n}\n","# Authoring configs by hand\n\nA Typecaast simulation is **one JSON config**. The visual playground\n(<https://typecaast.com/playground>) is the easy way to build one, but the config\nis plain JSON — you can write or edit it by hand in any editor (or have an LLM do\nit). This is the reference for doing that without the playground.\n\n> **Timing lives in its own guide.** \"Why are my messages too fast / too close\n> together?\" and \"how do I get ~1–2s between messages?\" are answered in\n> [pacing.md](./pacing.md). Read that one for anything about gaps, delays, and\n> typing speed.\n\n## The shape\n\n```json\n{\n \"$schema\": \"https://typecaast.com/schema/v1/typecaast.schema.json\",\n \"version\": 1,\n \"meta\": {\n \"canvas\": { \"width\": 880, \"height\": 720 },\n \"skin\": { \"id\": \"slack\" }\n },\n \"participants\": [\n { \"id\": \"me\", \"name\": \"You\", \"isSelf\": true },\n { \"id\": \"sam\", \"name\": \"Sam\" }\n ],\n \"pacing\": { \"readingWpm\": 240 },\n \"timeline\": [\n { \"type\": \"message\", \"from\": \"sam\", \"text\": \"ship it?\" },\n { \"type\": \"message\", \"from\": \"me\", \"text\": \"shipping 🚀\" }\n ]\n}\n```\n\nThat's a complete, valid config. Five top-level keys:\n\n| Key | Required | What it is |\n| -------------- | -------- | --------------------------------------------------------------------- |\n| `version` | yes | Config schema version. Currently `1` (a literal). |\n| `meta` | yes | Canvas, skin, theme, fps — how it renders. |\n| `participants` | yes | Who is in the conversation. |\n| `pacing` | no | Global timing model. Omit for defaults. See [pacing.md](./pacing.md). |\n| `timeline` | yes | The ordered list of steps that play out. |\n\nAdd the `$schema` line at the top and your editor gives you autocomplete and\ninline validation against the live schema. It's optional but recommended.\n\n## `meta`\n\n```json\n\"meta\": {\n \"canvas\": { \"width\": 880, \"height\": 720 },\n \"skin\": { \"id\": \"slack\", \"options\": { \"channel\": \"#alerts\" } },\n \"fps\": 30,\n \"fit\": \"reflow\",\n \"theme\": \"auto\",\n \"seed\": 42,\n \"background\": \"transparent\",\n \"assets\": \"inline\",\n \"composer\": \"auto\",\n \"loop\": false\n}\n```\n\n| Field | Default | Notes |\n| ------------ | --------------- | ----------------------------------------------------------------------------------------------------- |\n| `canvas` | — | `{ width, height }` in px. Required. The authoring reference size / the video frame. |\n| `skin` | — | `{ id, options? }`. Required. `id` picks the skin (see [Skins](#skins)); `options` are skin-specific. |\n| `fps` | `30` | Frames per second for video export. |\n| `fit` | `\"reflow\"` | `reflow` (re-wrap to container), `scale` (CSS-scale the canvas), or `fixed` (pin to px). |\n| `theme` | `\"auto\"` | `light`, `dark`, or `auto` (follows the host page; video resolves `auto` → `light`). |\n| `seed` | `42` | Seeds all deterministic jitter — same seed ⇒ identical timings. |\n| `background` | `\"transparent\"` | `\"transparent\"` or any CSS color. |\n| `assets` | `\"inline\"` | `inline` (embed images as data URLs) or `url` (reference hosted images). |\n| `composer` | `\"auto\"` | Reply box: `auto` (shown only while typing/sending), `always`, or `never`. |\n| `loop` | `false` | Auto-replay at the end (unless the `<Typecaast>` consumer passes an explicit `loop`). |\n\n## `participants`\n\nAn array of speakers. Reference them everywhere by `id`.\n\n```json\n\"participants\": [\n { \"id\": \"me\", \"name\": \"You\", \"isSelf\": true },\n { \"id\": \"sam\", \"name\": \"Sam\", \"avatar\": \"https://…/sam.png\", \"color\": \"#5b3a8e\" },\n { \"id\": \"bot\", \"name\": \"PostHog\", \"kind\": \"app\" }\n]\n```\n\n| Field | Required | Notes |\n| -------- | -------- | -------------------------------------------------------------------------------- |\n| `id` | yes | Stable id used by `from` / `target` / `by` in the timeline. |\n| `name` | yes | Display name. |\n| `avatar` | no | Data URL, or a hosted URL (per `meta.assets`). |\n| `color` | no | Accent color (CSS) some skins use for the author. |\n| `isSelf` | no | Marks the viewer — rendered on the \"self\" side and as the composer's author. |\n| `kind` | no | `\"person\"` (default) or `\"app\"` (a bot/integration; skins render it distinctly). |\n\nSet `isSelf: true` on exactly one participant if you use the composer\n(`composerType` / `send`).\n\n## `timeline` — the steps\n\nThe timeline is an ordered array. Each step has a `type` and its own fields. Two\nfields are shared by **every** step:\n\n- `id` — optional. Give a step an id so a later `reaction`, `edit`, `delete`, or\n `readReceipt` can target it.\n- `instant` — optional `true` to reveal with no animation and **no computed\n pacing gap** (handy when you want to control spacing yourself — see\n [pacing.md](./pacing.md)).\n\nSteps that target an earlier message take a `target`: either a message's `id` or\nthe literal `\"$prev\"` (the most-recent message). `target` defaults to `\"$prev\"`.\n\n### The step types\n\n| `type` | Purpose |\n| -------------- | ------------------------------------------------------------------------ |\n| `message` | An incoming message (optionally preceded by a typing indicator). |\n| `typing` | A standalone typing indicator (no message need follow). |\n| `composerType` | The `isSelf` participant typing into the composer, char by char. |\n| `send` | Commit the composer's current text to the thread. |\n| `reaction` | An emoji reaction landing on a target message. |\n| `edit` | Replace a previously sent message's body. |\n| `delete` | Remove a previously sent message. |\n| `readReceipt` | A read-receipt indicator (skins that support it). |\n| `system` | A system / notice line (e.g. \"Sam joined #alerts\") — not a chat message. |\n| `delay` | An explicit pause on the timeline. See [pacing.md](./pacing.md). |\n\n### `message`\n\n```json\n{ \"type\": \"message\", \"from\": \"sam\", \"text\": \"on it\" }\n```\n\n| Field | Notes |\n| --------- | ----------------------------------------------------------------------------------------------------- |\n| `from` | Participant id. Required. |\n| `text` | Message body. Supports Slack-style mrkdwn — see [message-content.md](./message-content.md). |\n| `images` | `[{ src, alt?, width?, height? }]` — in-message images. |\n| `content` | Explicit Block Kit content nodes (wins over `text`) — see [message-content.md](./message-content.md). |\n| `typing` | Show a typing indicator first: `true`, or `{ \"showTypingFor\": 1800 }` (ms). |\n\nModel an app \"card\" as a `message` from an `app` participant carrying `content`\n(header/section/context/actions/…), **not** a `system` step.\n\n### `typing`\n\n```json\n{ \"type\": \"typing\", \"from\": \"sam\", \"showTypingFor\": 1500 }\n```\n\n`showTypingFor` is in **milliseconds** (defaults to ~1500ms if omitted).\n\n### `composerType` and `send`\n\n```json\n{ \"type\": \"composerType\", \"from\": \"me\", \"text\": \"let me check…\" },\n{ \"type\": \"send\" }\n```\n\n`composerType` types into the composer (the `from` should be your `isSelf`\nparticipant); `send` then posts it. `send.from` is optional — it inherits the\ncomposer's author. `composerType.typingDuration` (ms) overrides the computed\ntyping time.\n\n### `reaction`\n\n```json\n{ \"type\": \"reaction\", \"target\": \"$prev\", \"emoji\": \"🚀\", \"delay\": 800 }\n```\n\n| Field | Notes |\n| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `emoji` | The emoji. Required. |\n| `target` | Message id or `\"$prev\"` (default). |\n| `from` | Optional reactor. |\n| `shortcode` | Optional shortcode without colons (e.g. `\"rocket\"`) — shown in some skins' tooltips. |\n| `delay` | Gap (ms) from when the target appears before the reaction lands. **This `delay` is a field on the reaction, not the `delay` step type** — don't confuse them (see [pacing.md](./pacing.md)). |\n\n### `edit` and `delete`\n\n```json\n{ \"type\": \"edit\", \"target\": \"m2\", \"text\": \"fixed typo\" },\n{ \"type\": \"delete\", \"target\": \"m3\" }\n```\n\nBoth default `target` to `\"$prev\"`. `edit` takes the same body fields as\n`message` (`text` / `images` / `content`).\n\n### `readReceipt`\n\n```json\n{ \"type\": \"readReceipt\", \"by\": \"sam\", \"target\": \"m2\" }\n```\n\n`by` (who read it) and `target` (up to which message) are both optional.\n\n### `system`\n\n```json\n{ \"type\": \"system\", \"text\": \"Sam joined #alerts\" }\n```\n\nA centered notice / tool-output line, rendered distinctly per skin. Takes the\nsame body fields as `message`.\n\n### `delay`\n\n```json\n{ \"type\": \"delay\", \"duration\": 1500 }\n```\n\nAn explicit pause. `duration` is in **milliseconds** and is **required**. This is\nthe most predictable way to space messages out — see [pacing.md](./pacing.md).\n\n## Skins\n\n`meta.skin.id` selects the skin. The conversation content is the same regardless\nof skin; the skin decides how it looks. Built-in ids:\n\n`slack` · `telegram` · `claude-code` · `imessage` · `messages-macos` ·\n`whatsapp` · `cursor` · `discord`\n\nSome steps aren't rendered by every skin (e.g. a terminal skin has no\nreactions). The skin's capabilities decide; unsupported steps are dropped. Pick a\nskin that supports the steps you use.\n\n## Validate it\n\nAlways validate after editing by hand:\n\n- **CLI:** `npx @typecaast/cli validate my-config.json` (exit 0 = OK; prints\n `E_*` errors / `W_*` warnings with locations and hints — see\n [errors.md](./errors.md)).\n- **MCP:** if you've added the [`@typecaast/mcp`](../packages/mcp/README.md)\n server to your editor, call its `validate_config` tool. This is the easy loop\n when drafting a config (e.g. from a screenshot) in your own project.\n\n## See also\n\n- [pacing.md](./pacing.md) — timing, delays, and the \"1–2s between messages\" recipes.\n- [message-content.md](./message-content.md) — message bodies: mrkdwn + Block Kit.\n- The JSON Schema: <https://typecaast.com/schema/v1/typecaast.schema.json>.\n- Machine-readable index for LLMs: <https://typecaast.com/llms.txt>.\n </content>\n","# Pacing & timing\n\nTypecaast **auto-paces** a conversation so you don't have to time every step by\nhand: it derives the gap before each message from how long the previous message\ntakes to \"read\", and how long typing takes from the text length. This is why a\nconfig with no timing fields still plays at a human rhythm.\n\nIt's also the #1 source of \"why is this too fast / why are my messages bunched\nup?\" — including the common ask, **\"I want ~1–2s between messages.\"** This guide\nexplains the model and the levers. (For the full step reference, see\n[authoring-configs.md](./authoring-configs.md).)\n\n## The model\n\nTiming is controlled by the optional top-level `pacing` object. Omit it and you\nget the full default model:\n\n```json\n\"pacing\": {\n \"readingWpm\": 240,\n \"typingCps\": 14,\n \"humanize\": 0.15,\n \"startDelayMs\": 400\n}\n```\n\n| Field | Default | Unit | What it controls |\n| -------------- | ------- | --------- | ------------------------------------------------------------------------------- |\n| `readingWpm` | `240` | words/min | The gap **before an incoming message** ≈ the time to read the **previous** one. |\n| `typingCps` | `14` | chars/sec | Composer typing speed, and how long a typing indicator shows before a message. |\n| `humanize` | `0.15` | fraction | ±15% **seeded** jitter on computed gaps/durations so it doesn't feel robotic. |\n| `startDelayMs` | `400` | ms | Delay before the very first event. |\n\nEvery value is overridable per step in the timeline; per-step values win over the\ncomputed ones.\n\n### How the gap is computed\n\nThe key thing to understand: **the gap before a message scales with the length of\nthe _previous_ message**, because it models the other person reading it before\nreplying. Roughly:\n\n```\nwords ≈ characters / 5\ngap(ms) ≈ (words / readingWpm) × 60000 (then ±humanize jitter)\n```\n\nSo at the default 240 wpm:\n\n- A short line like `\"ship it?\"` (~1.6 words) → gap ≈ **400ms**.\n- A sentence of ~40 characters (~8 words) → gap ≈ **2000ms**.\n\n**This is the gotcha:** with short messages, the default gaps are short (a few\nhundred ms), so the conversation feels snappy/bunched. Lowering `readingWpm` or\nadding explicit `delay` steps is how you stretch it out.\n\n### Other automatic durations\n\n- Typing indicator before a message (`message.typing: true`) lasts about as long\n as it would take to type that message at `typingCps`. A standalone `typing`\n step with no `showTypingFor` defaults to ~1500ms.\n- A `reaction` with no `delay` lands ~1000ms after its target appears.\n- `instant: true` on a step skips the reveal animation **and** the computed gap.\n\n### Determinism\n\nAll jitter is seeded from `meta.seed` (default `42`). The same `(config, seed)`\nalways produces identical timings — there is no `Math.random()`. Change `seed`\nto get a different (but still repeatable) jitter pattern.\n\n## \"I want ~1–2 seconds between messages\"\n\nFour ways, most predictable first.\n\n### 1. Insert a `delay` step (recommended — exact and explicit)\n\nA `delay` step is a fixed pause whose `duration` is in **milliseconds**:\n\n```json\n\"timeline\": [\n { \"type\": \"message\", \"from\": \"sam\", \"text\": \"ship it?\" },\n { \"type\": \"delay\", \"duration\": 1500 },\n { \"type\": \"message\", \"from\": \"me\", \"text\": \"shipping 🚀\" }\n]\n```\n\nThat guarantees ~1.5s between those two messages regardless of their length. Use\nit between each pair of messages you want spaced out. It's the most reliable knob\nbecause it doesn't depend on text length.\n\n> **Don't confuse the two `delay`s.** The `delay` **step type** (`{ \"type\":\n\"delay\", \"duration\": … }`) is a standalone pause. A `reaction` step also has a\n> `delay` **field** — that one is the gap before the _reaction_ lands on its\n> target. Different things.\n\n### 2. Lower `readingWpm` globally\n\nSlowing the \"reading speed\" lengthens every auto-computed gap at once:\n\n```json\n\"pacing\": { \"readingWpm\": 80 }\n```\n\nAt ~80 wpm a short `\"ship it?\"` reply gaps ~1.2s; longer messages gap\nproportionally more. Good for a uniformly slower feel. **Caveat:** because the\ngap still scales with text length, very short messages stay shorter than very\nlong ones — if you need a precise gap, use a `delay` step (#1).\n\nRough guide for a short one-liner: ~120 wpm ≈ 0.8s, ~80 wpm ≈ 1.2s, ~60 wpm ≈\n1.6s.\n\n### 3. `instant` + `delay` for full manual control\n\nDrop the automatic gaps entirely and place every pause yourself:\n\n```json\n{ \"type\": \"message\", \"from\": \"sam\", \"text\": \"ship it?\", \"instant\": true },\n{ \"type\": \"delay\", \"duration\": 1200 },\n{ \"type\": \"message\", \"from\": \"me\", \"text\": \"shipping 🚀\", \"instant\": true }\n```\n\n`instant` removes a step's computed gap and reveal animation, so the only spacing\nis your `delay` steps. Use this when you want a precisely choreographed timeline.\n\n### 4. Pad with a typing indicator\n\nA typing indicator before a message both adds time and reads naturally:\n\n```json\n{ \"type\": \"typing\", \"from\": \"sam\", \"showTypingFor\": 1500 },\n{ \"type\": \"message\", \"from\": \"sam\", \"text\": \"shipping 🚀\" }\n```\n\nor inline on the message:\n\n```json\n{\n \"type\": \"message\",\n \"from\": \"sam\",\n \"text\": \"shipping 🚀\",\n \"typing\": { \"showTypingFor\": 1500 }\n}\n```\n\n`showTypingFor` is in **milliseconds**.\n\n## Worked example\n\nTwo short messages with a deliberate 1.5s gap:\n\n```json\n{\n \"$schema\": \"https://typecaast.com/schema/v1/typecaast.schema.json\",\n \"version\": 1,\n \"meta\": {\n \"canvas\": { \"width\": 600, \"height\": 400 },\n \"skin\": { \"id\": \"imessage\" }\n },\n \"participants\": [\n { \"id\": \"me\", \"name\": \"You\", \"isSelf\": true },\n { \"id\": \"sam\", \"name\": \"Sam\" }\n ],\n \"timeline\": [\n { \"type\": \"message\", \"from\": \"sam\", \"text\": \"ship it?\" },\n { \"type\": \"delay\", \"duration\": 1500 },\n { \"type\": \"message\", \"from\": \"me\", \"text\": \"shipping 🚀\" }\n ]\n}\n```\n\nTimeline: first message reveals at ~`startDelayMs` (400ms); the `delay` holds\n1500ms; the reply reveals ~1.5s later. Without the `delay`, that gap would be the\ndefault ~400ms reading time of `\"ship it?\"`.\n\n## See also\n\n- [authoring-configs.md](./authoring-configs.md) — the full config + step reference.\n- [message-content.md](./message-content.md) — message bodies (mrkdwn + Block Kit).\n </content>\n","# Authoring message content\n\nA message's body is authored in its `content`. The simplest form is a plain\n`text` string; richer messages (especially **app messages**) use **Block Kit**\ncontent nodes — the same primitives a real Slack app emits.\n\n> The easiest way to author this is the [playground](https://typecaast.com/playground)\n> — it has a typed block editor. This doc is the reference for the JSON it\n> produces. Point your editor at the bundled JSON Schema\n> (`\"$schema\": \".../typecaast.schema.json\"`) for autocomplete.\n\n## Plain text + mrkdwn\n\nMost messages just need `text`. It's parsed for inline marks:\n\n```json\n{ \"type\": \"message\", \"from\": \"cory\", \"text\": \"shipped *it* — see `deploy.ts`\" }\n```\n\n| Syntax | Renders | Inline node |\n| ---------------------- | ----------------------------------------------------- | ------------ |\n| `*bold*` | **bold** | `bold` |\n| `_italic_` | _italic_ | `italic` |\n| `~strike~` | ~~strike~~ | `strike` |\n| `` `code` `` | `code` | `code` |\n| `https://…` | a link | `link` |\n| `@name` | a mention pill | `mention` |\n| `<@id>` | a mention resolved to that participant's display name | `mention` |\n| emoji (`🟠`, `:tada:`) | the glyph | left in text |\n\n`<@id>` is the robust way to mention someone whose display name has spaces\n(`<@joe>` → \"@Joe Saunderson\"); it resolves against `participants` at compile.\n\nIn-message images use the `images` sugar:\n\n```json\n{\n \"type\": \"message\",\n \"from\": \"cory\",\n \"text\": \"here's the toast:\",\n \"images\": [{ \"src\": \"./toast.png\", \"alt\": \"error toast\", \"width\": 320 }]\n}\n```\n\n## App messages (Block Kit)\n\nAn **app message** is a normal `message` from a participant with\n`\"kind\": \"app\"` — the sender drives the \"APP\" badge — whose `content` is a list\nof Block Kit nodes. **Slack renders the full set; other skins show the text and\nskip the rest** (a Slack-targeted config degrades gracefully elsewhere).\n\nBlocks carry a `text` string (parsed like above) or pre-resolved `spans`:\n\n```json\n{\n \"type\": \"message\",\n \"from\": \"posthog\",\n \"content\": [\n { \"type\": \"header\", \"text\": \"perf(inbox): Fix 26s admin inbox load\" },\n {\n \"type\": \"context\",\n \"elements\": [\n {\n \"type\": \"text\",\n \"text\": \"🟠 *P2* · Session replay · rvenvy/rvenvy-ai\"\n }\n ]\n },\n { \"type\": \"section\", \"text\": \"Sales reps hit prolonged loading spinners…\" },\n {\n \"type\": \"context\",\n \"elements\": [\n {\n \"type\": \"text\",\n \"text\": \"2 signals · Suggested reviewers: <@joe> <@cory>\"\n }\n ]\n },\n { \"type\": \"divider\" },\n {\n \"type\": \"actions\",\n \"elements\": [\n { \"type\": \"button\", \"label\": \"Review PR\", \"href\": \"https://…\" },\n { \"type\": \"button\", \"label\": \"Open in PostHog\" },\n { \"type\": \"button\", \"label\": \"Dismiss\", \"style\": \"danger\" }\n ]\n }\n ]\n}\n```\n\n### Block types\n\n| Node | Shape | Notes |\n| ------------ | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `header` | `{ text }` | Large bold heading (plain text). |\n| `section` | `{ text \\| spans, accessory?, fields? }` | A paragraph. `accessory` is a `button` or `image` to its right; `fields` is a 2-column grid of `{ text \\| spans }`. |\n| `context` | `{ elements: [...] }` | Small muted row; each element is `{ type:\"text\", text }` or `{ type:\"image\", src, alt? }`. |\n| `divider` | `{}` | A horizontal rule. |\n| `actions` | `{ elements: [button…] }` | A row of buttons. |\n| `image` | `{ src, alt?, width?, height? }` | A standalone image. |\n| `codeblock` | `{ text, lang? }` | A monospaced preformatted box (Slack's fenced ` ``` ` block). `text` is **literal** — whitespace and newlines are kept and it is **not** parsed for marks. |\n| `attachment` | `{ color?, content: [...] }` | Nested blocks behind a colored left bar (the legacy \"attachment\" look). `color` is any CSS color. |\n\n### Buttons\n\nA `button` element appears in an `actions` block or as a `section` accessory:\n\n```json\n{\n \"type\": \"button\",\n \"label\": \"View PR\",\n \"href\": \"https://…\",\n \"style\": \"primary\"\n}\n```\n\n- `style`: `\"primary\"` (filled green), `\"danger\"` (red outline), or omit for the\n default outlined button.\n- `href`: with one, the button is a real link (opens in a new tab); without one\n it renders inert.\n\n### Code blocks\n\nFor a table, log, or snippet, use a `codeblock` — Slack's fenced ` ``` `\nblock, a monospaced box that preserves whitespace and newlines verbatim. Unlike\nthe inline `` `code` `` mark (a single-line pill inside a sentence), a `codeblock`\nis its own block and its `text` is **literal**: `*`, `~`, `<@id>` etc. are shown\nas typed, never parsed. Author the canvas wide enough that the widest line fits on\none line, then scale the embed down on the page (see below).\n\n```json\n{\n \"type\": \"message\",\n \"from\": \"posthog\",\n \"content\": [\n {\n \"type\": \"codeblock\",\n \"text\": \"Metric A B\\nReturned 64% 96%\\nActive weeks 2.9 7.2\"\n }\n ]\n}\n```\n\nA wide monospace block won't reflow, so give the instance a wide\n`meta.canvas.width` and set `\"fit\": \"scale\"`; the host wraps it in a sized\ncontainer and the whole instance scales down to fit (the canvas keeps its internal\npixel width, so the table stays one line per row).\n\n### The colored bar (attachment)\n\nTo get the legacy left-bar card look, wrap blocks in an `attachment`:\n\n```json\n{\n \"type\": \"message\",\n \"from\": \"posthog\",\n \"content\": [\n {\n \"type\": \"attachment\",\n \"color\": \"#36a64f\",\n \"content\": [\n { \"type\": \"section\", \"text\": \"Pull request opened.\" },\n {\n \"type\": \"actions\",\n \"elements\": [\n { \"type\": \"button\", \"label\": \"View PR\", \"style\": \"primary\" }\n ]\n }\n ]\n }\n ]\n}\n```\n\n## System / notice lines\n\nThe `system` step is **not** an app card — it's a non-message notice line\n(\"X joined #channel\", an agent's tool-output line), rendered distinctly per skin\n(centered/muted in Slack, iMessage, WhatsApp; an agent line in Cursor/Claude\nCode). It carries only `from?` + text/`content`:\n\n```json\n{ \"type\": \"system\", \"from\": \"posthog\", \"text\": \"Cory joined #signals\" }\n```\n\n## The reply box\n\nThe composer (\"reply box\") visibility is set by `meta.composer`: `\"auto\"`\n(default — shown only while someone is composing), `\"always\"` (always shown,\nidle when nobody is typing — no `isSelf` participant required), or `\"never\"`.\n","// The authoring guides, inlined as strings at build time (esbuild `text`\n// loader). Source of truth is the repo's /docs — these are the same files the\n// site serves. Kept out of core.ts so the unit tests don't need a .md loader.\nimport authoringConfigs from \"../../../docs/authoring-configs.md\";\nimport pacing from \"../../../docs/pacing.md\";\nimport messageContent from \"../../../docs/message-content.md\";\n\nexport interface DocEntry {\n slug: string;\n title: string;\n text: string;\n}\n\nexport const DOCS: DocEntry[] = [\n {\n slug: \"authoring-configs\",\n title: \"Authoring configs by hand\",\n text: authoringConfigs,\n },\n { slug: \"pacing\", title: \"Pacing & timing\", text: pacing },\n {\n slug: \"message-content\",\n title: \"Message content\",\n text: messageContent,\n },\n];\n","// Pure, dependency-light helpers — no MCP SDK, no .md imports — so they're\n// trivially unit-testable. The SDK wiring lives in server.ts.\nimport {\n validateConfig,\n configJsonSchema,\n type Diagnostic,\n} from \"@typecaast/schema\";\nimport skinsRegistry from \"../../../registry/skins.json\";\n\nexport const SCHEMA_ID =\n \"https://typecaast.com/schema/v1/typecaast.schema.json\";\n\nexport interface SkinInfo {\n id: string;\n name: string;\n themes: string[];\n summary: string;\n}\n\n/**\n * Slim built-in skin manifest derived from registry/skins.json — no React, so\n * the server stays Node-pure. (Capabilities aren't included to avoid importing\n * the skin components; `themes` + `summary` are enough to pick one.)\n */\nexport const SKINS: SkinInfo[] = skinsRegistry.skins\n .filter((s) => s.official)\n .map((s) => ({\n id: s.id,\n name: s.name,\n themes: s.themes,\n summary: s.summary,\n }));\n\n/** The JSON Schema, wrapped with the same $id/title as the site + artifact. */\nexport function jsonSchema(): Record<string, unknown> {\n return {\n $schema: \"http://json-schema.org/draft-07/schema#\",\n $id: SCHEMA_ID,\n title: \"Typecaast config\",\n ...configJsonSchema(),\n };\n}\n\nexport interface ValidationResult {\n valid: boolean;\n diagnostics: Diagnostic[];\n /** Set when the input wasn't valid JSON (couldn't even be parsed). */\n error?: string;\n}\n\n/** Validate a config given as an object or a JSON string. */\nexport function validate(config: unknown): ValidationResult {\n let value = config;\n if (typeof config === \"string\") {\n try {\n value = JSON.parse(config);\n } catch (e) {\n return {\n valid: false,\n diagnostics: [],\n error: `Invalid JSON: ${(e as Error).message}`,\n };\n }\n }\n const diagnostics = validateConfig(value);\n const valid = !diagnostics.some((d) => d.severity === \"error\");\n return { valid, diagnostics };\n}\n\n/** A minimal valid config to start from, for the given skin (default slack). */\nexport function scaffoldConfig(skinId = \"slack\"): Record<string, unknown> {\n return {\n $schema: SCHEMA_ID,\n version: 1,\n meta: { canvas: { width: 600, height: 760 }, skin: { id: skinId } },\n participants: [\n { id: \"me\", name: \"You\", isSelf: true },\n { id: \"sam\", name: \"Sam\" },\n ],\n timeline: [\n { type: \"message\", from: \"sam\", text: \"ship it?\" },\n { type: \"delay\", duration: 1500 },\n { type: \"message\", from: \"me\", text: \"shipping 🚀\" },\n ],\n };\n}\n","{\n \"$schema\": \"./skins.schema.json\",\n \"version\": 1,\n \"skins\": [\n {\n \"id\": \"slack\",\n \"name\": \"Slack\",\n \"official\": true,\n \"author\": \"Typecaast\",\n \"package\": \"@typecaast/skins\",\n \"export\": \"slack\",\n \"themes\": [\"light\", \"dark\"],\n \"intendedFonts\": [\"Lato\"],\n \"summary\": \"Slack-style thread: app cards, reactions, replies.\",\n \"verified\": \"2026-Q2\"\n },\n {\n \"id\": \"telegram\",\n \"name\": \"Telegram\",\n \"official\": true,\n \"author\": \"Typecaast\",\n \"package\": \"@typecaast/skins\",\n \"export\": \"telegram\",\n \"themes\": [\"light\", \"dark\"],\n \"intendedFonts\": [\"Roboto\"],\n \"summary\": \"Telegram chat: bubbles with tails, reactions, bot inline buttons.\",\n \"verified\": \"2026-Q2\"\n },\n {\n \"id\": \"claude-code\",\n \"name\": \"Claude Code (TUI)\",\n \"official\": true,\n \"author\": \"Typecaast\",\n \"package\": \"@typecaast/skins\",\n \"export\": \"claudeCode\",\n \"themes\": [\"dark\"],\n \"intendedFonts\": [\"JetBrains Mono\"],\n \"summary\": \"Terminal UI with streaming output and a spinner.\",\n \"verified\": \"2026-Q2\"\n },\n {\n \"id\": \"imessage\",\n \"name\": \"iMessage (iOS)\",\n \"official\": true,\n \"author\": \"Typecaast\",\n \"package\": \"@typecaast/skins\",\n \"export\": \"imessage\",\n \"themes\": [\"light\", \"dark\"],\n \"intendedFonts\": [\"SF Pro (→ Inter)\"],\n \"summary\": \"iPhone Messages: bubbles, tapbacks, status bar, keyboard.\",\n \"verified\": \"2026-Q2\"\n },\n {\n \"id\": \"messages-macos\",\n \"name\": \"Messages (macOS)\",\n \"official\": true,\n \"author\": \"Typecaast\",\n \"package\": \"@typecaast/skins\",\n \"export\": \"messagesMacos\",\n \"themes\": [\"light\", \"dark\"],\n \"intendedFonts\": [\"SF Pro (→ Inter)\"],\n \"summary\": \"Desktop Messages: window chrome + conversation sidebar.\",\n \"verified\": \"2026-Q2\"\n },\n {\n \"id\": \"whatsapp\",\n \"name\": \"WhatsApp\",\n \"official\": true,\n \"author\": \"Typecaast\",\n \"package\": \"@typecaast/skins\",\n \"export\": \"whatsapp\",\n \"themes\": [\"light\", \"dark\"],\n \"intendedFonts\": [\"Helvetica Neue (system)\"],\n \"summary\": \"WhatsApp: in-bubble timestamps + double-tick receipts.\",\n \"verified\": \"2026-Q2\"\n },\n {\n \"id\": \"cursor\",\n \"name\": \"Cursor panel\",\n \"official\": true,\n \"author\": \"Typecaast\",\n \"package\": \"@typecaast/skins\",\n \"export\": \"cursor\",\n \"themes\": [\"dark\", \"light\"],\n \"intendedFonts\": [\"gg sans (system)\"],\n \"summary\": \"Cursor AI side-panel: prompts, responses, model chips.\",\n \"verified\": \"2026-Q2\"\n },\n {\n \"id\": \"discord\",\n \"name\": \"Discord\",\n \"official\": true,\n \"author\": \"Typecaast\",\n \"package\": \"@typecaast/skins\",\n \"export\": \"discord\",\n \"themes\": [\"dark\"],\n \"intendedFonts\": [\"gg sans (→ Noto Sans)\"],\n \"summary\": \"Discord channel: role colors, grouped messages, reactions.\",\n \"verified\": \"2026-Q2\"\n }\n ]\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@typecaast/mcp",
3
+ "version": "0.1.0",
4
+ "description": "Model Context Protocol server for authoring & validating Typecaast configs.",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/corywatilo/typecaast.git",
9
+ "directory": "packages/mcp"
10
+ },
11
+ "type": "module",
12
+ "bin": {
13
+ "typecaast-mcp": "./dist/index.js"
14
+ },
15
+ "main": "./dist/index.js",
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "dev": "tsup --watch",
22
+ "typecheck": "tsc --noEmit",
23
+ "lint": "eslint src",
24
+ "test": "vitest run",
25
+ "clean": "rm -rf dist .turbo"
26
+ },
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.29.0",
29
+ "@typecaast/schema": "workspace:*",
30
+ "zod": "^4.4.3"
31
+ }
32
+ }