[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6GVAD8FTP9TCC06SSBSGNMM":3,"skill-same:01M6GVAD8FTP9TCC06SSBSGNMM":89},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":16,"sourceHtmlUrl":17,"categories":18,"keywords":24,"publishedAt":42,"linkOnly":43,"markdownContent":44,"markdownBody":45,"frontMatter":46,"refName":52,"commitSha":53,"skillDirectory":54,"forksCount":55,"repositoryHtmlUrl":56,"repositoryHomepageUrl":6,"ownerHtmlUrl":57,"ownerBlogUrl":58,"ownerDescription":59,"updatedAt":60,"install":61,"resources":74},"01M6GVAD8FTP9TCC06SSBSGNMM","add-app-tool",null,"Scaffold an MCP App tool + UI resource pair. Use when the user asks to add a tool with interactive UI, create an MCP App, or build a visual\u002Finteractive tool.","jiayaoqijia\u002Fcryptoskill","jiayaoqijia","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F1453879?v=4","01M6GMYTX7Q7S504VC6E756ENR","YQ","01M6GMYV1QKJ1CC75MTTGT9PR0",80,"Apache-2.0","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fmcp-servers\u002Fcyanheads-add-app-tool\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fmcp-servers\u002Fcyanheads-add-app-tool\u002FSKILL.md",[19],{"categoryNo":20,"categoryName":21,"taxonomyType":22,"taxonomyTypeDesc":23},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",[25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41],"add app tool","tool","add","app","use","when","with","visual","build","reference","MCP","user","interactive","pair","ui","create","scaffold","2026-11-10 10:12:30",false,"---\nname: add-app-tool\ndescription: >\n  Scaffold an MCP App tool + UI resource pair. Use when the user asks to add a tool with interactive UI, create an MCP App, or build a visual\u002Finteractive tool.\nmetadata:\n  author: cyanheads\n  version: \"1.8\"\n  audience: external\n  type: reference\n---\n\n## When to Use\n\nApp tools are **rarely the right choice**. Reach for one only when all of the following hold:\n\n1. A *human* will actively interact with the result in real time — not just an LLM consuming text.\n2. The target deployment runs in a client that supports MCP Apps. Many clients (Claude Code, Cursor, most chat UIs) are tool-only and will only ever see the `format()` text fallback you have to maintain anyway.\n3. The interaction the UI enables — scrubbing a dense table, approving a multi-step plan, filling a structured form — is core to the workflow, not nice-to-have rendering.\n\nApp tools cost more than standard tools: an iframe + CSP setup, `app.ontoolresult` \u002F `callServerTool` plumbing, host-context wiring (theme, fonts, styles), and a `format()` text path that has to be content-complete because most clients see only that. Two surfaces to keep in sync, two failure modes per change.\n\nDefault to `add-tool`. This skill is the how-to once that bar is cleared — the \"whether to\" decision belongs in `design-mcp-server`.\n\n## Context\n\nMCP Apps extend the standard tool pattern with an interactive HTML UI rendered in a sandboxed iframe by the host. Each MCP App consists of two definitions:\n\n1. **App tool** (`.app-tool.ts`) — uses `appTool()` builder, declares `resourceUri` pointing to the UI resource\n2. **App resource** (`.app-resource.ts`) — uses `appResource()` builder, serves the bundled HTML\n\nBoth builders are exported from `@cyanheads\u002Fmcp-ts-core`. They handle `_meta.ui.resourceUri`, the compat key (`ui\u002FresourceUri`), and the correct MIME type (`text\u002Fhtml;profile=mcp-app`) automatically.\n\nFor the full API, Context interface, and error codes, read the framework's `CLAUDE.md`\u002F`AGENTS.md` (loaded at session start).\n\n## Steps\n\n1. **Confirm the three conditions** in \"When to Use\" apply — if any is uncertain, default to `add-tool` instead. Then **gather** the tool's name, purpose, input\u002Foutput shape, and what the UI should display from the user's request — ask only if genuinely absent\n2. **Choose a URI** — convention: `ui:\u002F\u002F{{tool-name}}\u002Fapp.html`\n3. **Create the app tool** at `src\u002Fmcp-server\u002Ftools\u002Fdefinitions\u002F{{tool-name}}.app-tool.ts`\n4. **Create the app resource** at `src\u002Fmcp-server\u002Fresources\u002Fdefinitions\u002F{{tool-name}}-ui.app-resource.ts`\n5. **Register both** in the project's existing `createApp()` arrays (directly in `src\u002Findex.ts` for fresh scaffolds, or via barrels if the repo already has them)\n6. **Run `bun run devcheck`** — the linter validates `_meta.ui` and cross-checks tool\u002Fresource pairing\n7. **Smoke-test** with `bun run rebuild && bun run start:stdio \u003C \u002Fdev\u002Fnull` (or `start:http`) — the `Core services constructed` log record must list the tool in its `tools` field and its UI resource in `resources` (the message text shows only counts)\n\n## App Tool Template\n\n```typescript\n\u002F**\n * @fileoverview {{TOOL_DESCRIPTION}}\n * @module mcp-server\u002Ftools\u002Fdefinitions\u002F{{TOOL_NAME}}.app-tool\n *\u002F\n\nimport { appTool, z } from '@cyanheads\u002Fmcp-ts-core';\n\nconst UI_RESOURCE_URI = 'ui:\u002F\u002F{{tool-name}}\u002Fapp.html';\n\nexport const {{TOOL_EXPORT}} = appTool('{{tool_name}}', {\n  resourceUri: UI_RESOURCE_URI,\n  title: '{{TOOL_TITLE}}',\n  description: '{{TOOL_DESCRIPTION}}',\n  annotations: { readOnlyHint: true },\n  input: z.object({\n    \u002F\u002F All fields need .describe(). Only JSON-Schema-serializable Zod types allowed.\n  }),\n  output: z.object({\n    \u002F\u002F All fields need .describe(). Only JSON-Schema-serializable Zod types allowed.\n  }),\n  \u002F\u002F auth: ['tool:{{tool_name}}:read'],\n\n  async handler(input, ctx) {\n    ctx.log.info('Processing', { \u002F* relevant input fields *\u002F });\n    return { \u002F* output *\u002F };\n  },\n\n  \u002F\u002F format() serves dual purpose for app tools:\n  \u002F\u002F 1. First text block: JSON for the UI (app.ontoolresult parses it)\n  \u002F\u002F 2. Subsequent blocks: human-readable, content-complete fallback for non-app hosts and LLM context\n  format(result) {\n    return [\n      { type: 'text', text: JSON.stringify(result) },\n      { type: 'text', text: '\u002F* human-readable summary with all LLM-needed fields *\u002F' },\n    ];\n  },\n});\n```\n\n## App Resource Template\n\n```typescript\n\u002F**\n * @fileoverview UI resource for {{TOOL_NAME}}.\n * @module mcp-server\u002Fresources\u002Fdefinitions\u002F{{TOOL_NAME}}-ui.app-resource\n *\u002F\n\nimport { appResource, z } from '@cyanheads\u002Fmcp-ts-core';\n\nconst ParamsSchema = z.object({}).describe('No parameters. Returns the static HTML app.');\n\nconst APP_HTML = `\u003C!DOCTYPE html>\n\u003Chtml lang=\"en\">\n\u003Chead>\n  \u003Cmeta charset=\"utf-8\" \u002F>\n  \u003Cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\" \u002F>\n  \u003Ctitle>{{TOOL_TITLE}}\u003C\u002Ftitle>\n  \u003Cstyle>\u002F* your styles *\u002F\u003C\u002Fstyle>\n\u003C\u002Fhead>\n\u003Cbody>\n  \u003C!-- your UI markup -->\n\n  \u003Cscript type=\"module\">\n    \u002F\u002F PROTOTYPING ONLY — replace before shipping. Bundle via Vite +\n    \u002F\u002F vite-plugin-singlefile or inline the SDK. Live CDN imports require\n    \u002F\u002F CSP whitelisting, add supply-chain risk, and break offline use.\n    \u002F\u002F See UI Notes below.\n    import {\n      App,\n      applyDocumentTheme,\n      applyHostFonts,\n      applyHostStyleVariables,\n    } from \"https:\u002F\u002Funpkg.com\u002F@modelcontextprotocol\u002Fext-apps@2\u002Fapp-with-deps\";\n\n    const app = new App({ name: \"{{TOOL_TITLE}}\", version: \"1.0.0\" });\n\n    function applyHostContext(hostContext) {\n      if (hostContext?.theme) {\n        applyDocumentTheme(hostContext.theme);\n      }\n      if (hostContext?.styles?.variables) {\n        applyHostStyleVariables(hostContext.styles.variables);\n      }\n      if (hostContext?.styles?.css?.fonts) {\n        applyHostFonts(hostContext.styles.css.fonts);\n      }\n    }\n\n    \u002F\u002F Receive initial tool result from the host\n    app.ontoolresult = (result) => {\n      const text = result.content?.find(c => c.type === \"text\")?.text;\n      if (!text) return;\n      const data = JSON.parse(text);\n      \u002F\u002F render data into the DOM\n    };\n    app.onhostcontextchanged = applyHostContext;\n\n    \u002F\u002F Proactively call tools from the UI\n    document.getElementById(\"action-btn\").addEventListener(\"click\", async () => {\n      const result = await app.callServerTool({\n        name: \"{{tool_name}}\",\n        arguments: { \u002F* input *\u002F },\n      });\n      \u002F\u002F handle result\n    });\n\n    app.connect().then(() => {\n      const hostContext = app.getHostContext();\n      if (hostContext) applyHostContext(hostContext);\n    });\n  \u003C\u002Fscript>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>`;\n\nexport const {{RESOURCE_EXPORT}} = appResource('ui:\u002F\u002F{{tool-name}}\u002Fapp.html', {\n  name: '{{tool-name}}-ui',\n  title: '{{TOOL_TITLE}} UI',\n  description: 'Interactive HTML app for {{tool_name}}.',\n  params: ParamsSchema,\n  \u002F\u002F auth: ['resource:{{tool-name}}-ui:read'],\n  _meta: {\n    ui: {\n      csp: { resourceDomains: ['https:\u002F\u002Funpkg.com'] },\n    },\n  },\n\n  handler(_params, ctx) {\n    ctx.log.debug('Serving app UI.', { resourceUri: ctx.uri?.href });\n    return APP_HTML;\n  },\n});\n```\n\nNo `list` callback: `ui:\u002F\u002F{{tool-name}}\u002Fapp.html` has no template variables, so it registers as a fixed resource that `resources\u002Flist` already reports. Registration ignores `list` on a variable-free URI.\n\n## UI Notes\n\n- **Ship self-contained HTML.** Author with Vite + `vite-plugin-singlefile` or inline the SDK. Live CDN imports in a `ui:\u002F\u002F` resource are a CSP footgun (every domain has to be whitelisted on `_meta.ui.csp.resourceDomains`), a supply-chain footgun (third-party JS executes inside the host's iframe), and a runtime footgun (every render needs network). The `unpkg` line in the template is for prototyping only.\n- **CSP.** MCP Apps iframes run under deny-by-default CSP. With `appResource()`, put `_meta.ui.csp.resourceDomains` on the definition; the builder mirrors it into returned `resources\u002Fread` content items. With plain `resource()`, attach `_meta.ui` yourself in `format()`.\n- **Adopt the host's visual identity, don't impose your own.** App UIs render inside the host's iframe alongside its native UI. Three host hooks layer on top of your CSS:\n  - `applyDocumentTheme(hostContext.theme)` — sets `color-scheme` and a `data-theme` attribute on `\u003Chtml>`\n  - `applyHostStyleVariables(hostContext.styles.variables)` — installs host CSS custom properties on `:root`. The MCP Apps spec fixes the names (`McpUiStyleVariableKey`): `--color-background-primary`, `--color-text-primary`, `--color-border-primary`, `--font-sans`, `--border-radius-md`, and so on. A host may send any subset.\n  - `applyHostFonts(hostContext.styles.css.fonts)` — installs `@font-face` rules for the host's font stack\n\n  Author CSS to *consume* these via `var(--color-background-primary, var(--bg))` — the host variable first, a local token as the fallback. Don't hardcode brand colors that fight the host.\n- **Pre-connect baseline.** `app.connect()` is async — host context arrives a frame or two after first paint. Without a baseline, the UI flashes unstyled or wrong-themed on light hosts. Ship local tokens with a light default, a `prefers-color-scheme` dark override, and a `data-theme` pin so the first frame is sensible:\n\n  ```css\n  :root { color-scheme: light dark; --bg: #fff; --fg: #111; }\n  @media (prefers-color-scheme: dark) { :root:not([data-theme=\"light\"]) { --bg: #0c0d12; --fg: #ededef; } }\n  :root[data-theme=\"dark\"] { --bg: #0c0d12; --fg: #ededef; }\n  body { background: var(--color-background-primary, var(--bg)); color: var(--color-text-primary, var(--fg)); }\n  ```\n\n  Once host context is applied, the host's variables win; the local tokens cover the gap before it arrives and any variable the host omits.\n- **`format()` for app tools.** The first `text` content block is typically JSON that the UI parses via `ontoolresult`. Additional blocks are the human-readable fallback that non-app hosts and LLMs consume — they must render every field the LLM needs to reason about. JSON-only payloads leave model-visible context blind.\n- **App resource `format()`.** `appResource()` already preserves raw HTML for the default app MIME type and mirrors definition `_meta.ui` into content items. Add a custom `format()` only when you need extra per-read metadata or non-default content shaping.\n\n## Registration\n\n```typescript\n\u002F\u002F src\u002Findex.ts (fresh scaffold default)\nimport { createApp } from '@cyanheads\u002Fmcp-ts-core';\nimport { {{TOOL_EXPORT}} } from '.\u002Fmcp-server\u002Ftools\u002Fdefinitions\u002F{{tool-name}}.app-tool.js';\nimport { {{RESOURCE_EXPORT}} } from '.\u002Fmcp-server\u002Fresources\u002Fdefinitions\u002F{{tool-name}}-ui.app-resource.js';\n\nawait createApp({\n  tools: [{{TOOL_EXPORT}}],\n  resources: [{{RESOURCE_EXPORT}}],\n  prompts: [\u002F* existing prompts *\u002F],\n});\n```\n\nIf the repo already uses `definitions\u002Findex.ts` barrels, update those instead of changing the registration pattern.\n\n## Checklist\n\n- [ ] App tool created at `src\u002Fmcp-server\u002Ftools\u002Fdefinitions\u002F{{tool-name}}.app-tool.ts` using `appTool()`\n- [ ] App resource created at `src\u002Fmcp-server\u002Fresources\u002Fdefinitions\u002F{{tool-name}}-ui.app-resource.ts` using `appResource()`\n- [ ] `resourceUri` matches between tool and resource (`ui:\u002F\u002F{{tool-name}}\u002Fapp.html`)\n- [ ] Zod schemas: all fields have `.describe()`, only JSON-Schema-serializable types\n- [ ] `format()` first block is `JSON.stringify(result)` — the full output object for the UI to parse via `app.ontoolresult`. Subsequent blocks are human-readable, content-complete fallback for non-app hosts and LLMs\n- [ ] App resource `_meta.ui.csp.resourceDomains` lists every external domain loaded by the UI\n- [ ] UI bundles or inlines the client SDK for the shipped HTML, and handles `app.ontoolresult`\n- [ ] UI applies host context updates via `app.onhostcontextchanged`\n- [ ] UI CSS reads host variables with local fallbacks (`var(--color-background-primary, var(--bg))`) over a light\u002Fdark baseline\n- [ ] No `list` callback on the fixed `ui:\u002F\u002F…\u002Fapp.html` resource — `resources\u002Flist` already reports it\n- [ ] Both registered in the project's existing `createApp()` arrays (directly or via barrels)\n- [ ] Handler tested directly via `createMockContext()`, or `add-test` skill run to scaffold the test file\n- [ ] `bun run devcheck` passes (linter validates `_meta.ui` and tool\u002Fresource pairing)\n- [ ] Smoke-tested with `bun run rebuild && bun run start:stdio \u003C \u002Fdev\u002Fnull` (or `start:http`); the `Core services constructed` record lists the tool in `tools` and its UI resource in `resources`\n- [ ] View rendered in the headless MCP Apps host (`mcp-ts-core app-render`; `field-test` skill, Step 6): `initialized` is true, `errors` and `cspViolations` are empty, and the screenshot shows the tool result\n","## When to Use\n\nApp tools are **rarely the right choice**. Reach for one only when all of the following hold:\n\n1. A *human* will actively interact with the result in real time — not just an LLM consuming text.\n2. The target deployment runs in a client that supports MCP Apps. Many clients (Claude Code, Cursor, most chat UIs) are tool-only and will only ever see the `format()` text fallback you have to maintain anyway.\n3. The interaction the UI enables — scrubbing a dense table, approving a multi-step plan, filling a structured form — is core to the workflow, not nice-to-have rendering.\n\nApp tools cost more than standard tools: an iframe + CSP setup, `app.ontoolresult` \u002F `callServerTool` plumbing, host-context wiring (theme, fonts, styles), and a `format()` text path that has to be content-complete because most clients see only that. Two surfaces to keep in sync, two failure modes per change.\n\nDefault to `add-tool`. This skill is the how-to once that bar is cleared — the \"whether to\" decision belongs in `design-mcp-server`.\n\n## Context\n\nMCP Apps extend the standard tool pattern with an interactive HTML UI rendered in a sandboxed iframe by the host. Each MCP App consists of two definitions:\n\n1. **App tool** (`.app-tool.ts`) — uses `appTool()` builder, declares `resourceUri` pointing to the UI resource\n2. **App resource** (`.app-resource.ts`) — uses `appResource()` builder, serves the bundled HTML\n\nBoth builders are exported from `@cyanheads\u002Fmcp-ts-core`. They handle `_meta.ui.resourceUri`, the compat key (`ui\u002FresourceUri`), and the correct MIME type (`text\u002Fhtml;profile=mcp-app`) automatically.\n\nFor the full API, Context interface, and error codes, read the framework's `CLAUDE.md`\u002F`AGENTS.md` (loaded at session start).\n\n## Steps\n\n1. **Confirm the three conditions** in \"When to Use\" apply — if any is uncertain, default to `add-tool` instead. Then **gather** the tool's name, purpose, input\u002Foutput shape, and what the UI should display from the user's request — ask only if genuinely absent\n2. **Choose a URI** — convention: `ui:\u002F\u002F{{tool-name}}\u002Fapp.html`\n3. **Create the app tool** at `src\u002Fmcp-server\u002Ftools\u002Fdefinitions\u002F{{tool-name}}.app-tool.ts`\n4. **Create the app resource** at `src\u002Fmcp-server\u002Fresources\u002Fdefinitions\u002F{{tool-name}}-ui.app-resource.ts`\n5. **Register both** in the project's existing `createApp()` arrays (directly in `src\u002Findex.ts` for fresh scaffolds, or via barrels if the repo already has them)\n6. **Run `bun run devcheck`** — the linter validates `_meta.ui` and cross-checks tool\u002Fresource pairing\n7. **Smoke-test** with `bun run rebuild && bun run start:stdio \u003C \u002Fdev\u002Fnull` (or `start:http`) — the `Core services constructed` log record must list the tool in its `tools` field and its UI resource in `resources` (the message text shows only counts)\n\n## App Tool Template\n\n```typescript\n\u002F**\n * @fileoverview {{TOOL_DESCRIPTION}}\n * @module mcp-server\u002Ftools\u002Fdefinitions\u002F{{TOOL_NAME}}.app-tool\n *\u002F\n\nimport { appTool, z } from '@cyanheads\u002Fmcp-ts-core';\n\nconst UI_RESOURCE_URI = 'ui:\u002F\u002F{{tool-name}}\u002Fapp.html';\n\nexport const {{TOOL_EXPORT}} = appTool('{{tool_name}}', {\n  resourceUri: UI_RESOURCE_URI,\n  title: '{{TOOL_TITLE}}',\n  description: '{{TOOL_DESCRIPTION}}',\n  annotations: { readOnlyHint: true },\n  input: z.object({\n    \u002F\u002F All fields need .describe(). Only JSON-Schema-serializable Zod types allowed.\n  }),\n  output: z.object({\n    \u002F\u002F All fields need .describe(). Only JSON-Schema-serializable Zod types allowed.\n  }),\n  \u002F\u002F auth: ['tool:{{tool_name}}:read'],\n\n  async handler(input, ctx) {\n    ctx.log.info('Processing', { \u002F* relevant input fields *\u002F });\n    return { \u002F* output *\u002F };\n  },\n\n  \u002F\u002F format() serves dual purpose for app tools:\n  \u002F\u002F 1. First text block: JSON for the UI (app.ontoolresult parses it)\n  \u002F\u002F 2. Subsequent blocks: human-readable, content-complete fallback for non-app hosts and LLM context\n  format(result) {\n    return [\n      { type: 'text', text: JSON.stringify(result) },\n      { type: 'text', text: '\u002F* human-readable summary with all LLM-needed fields *\u002F' },\n    ];\n  },\n});\n```\n\n## App Resource Template\n\n```typescript\n\u002F**\n * @fileoverview UI resource for {{TOOL_NAME}}.\n * @module mcp-server\u002Fresources\u002Fdefinitions\u002F{{TOOL_NAME}}-ui.app-resource\n *\u002F\n\nimport { appResource, z } from '@cyanheads\u002Fmcp-ts-core';\n\nconst ParamsSchema = z.object({}).describe('No parameters. Returns the static HTML app.');\n\nconst APP_HTML = `\u003C!DOCTYPE html>\n\u003Chtml lang=\"en\">\n\u003Chead>\n  \u003Cmeta charset=\"utf-8\" \u002F>\n  \u003Cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\" \u002F>\n  \u003Ctitle>{{TOOL_TITLE}}\u003C\u002Ftitle>\n  \u003Cstyle>\u002F* your styles *\u002F\u003C\u002Fstyle>\n\u003C\u002Fhead>\n\u003Cbody>\n  \u003C!-- your UI markup -->\n\n  \u003Cscript type=\"module\">\n    \u002F\u002F PROTOTYPING ONLY — replace before shipping. Bundle via Vite +\n    \u002F\u002F vite-plugin-singlefile or inline the SDK. Live CDN imports require\n    \u002F\u002F CSP whitelisting, add supply-chain risk, and break offline use.\n    \u002F\u002F See UI Notes below.\n    import {\n      App,\n      applyDocumentTheme,\n      applyHostFonts,\n      applyHostStyleVariables,\n    } from \"https:\u002F\u002Funpkg.com\u002F@modelcontextprotocol\u002Fext-apps@2\u002Fapp-with-deps\";\n\n    const app = new App({ name: \"{{TOOL_TITLE}}\", version: \"1.0.0\" });\n\n    function applyHostContext(hostContext) {\n      if (hostContext?.theme) {\n        applyDocumentTheme(hostContext.theme);\n      }\n      if (hostContext?.styles?.variables) {\n        applyHostStyleVariables(hostContext.styles.variables);\n      }\n      if (hostContext?.styles?.css?.fonts) {\n        applyHostFonts(hostContext.styles.css.fonts);\n      }\n    }\n\n    \u002F\u002F Receive initial tool result from the host\n    app.ontoolresult = (result) => {\n      const text = result.content?.find(c => c.type === \"text\")?.text;\n      if (!text) return;\n      const data = JSON.parse(text);\n      \u002F\u002F render data into the DOM\n    };\n    app.onhostcontextchanged = applyHostContext;\n\n    \u002F\u002F Proactively call tools from the UI\n    document.getElementById(\"action-btn\").addEventListener(\"click\", async () => {\n      const result = await app.callServerTool({\n        name: \"{{tool_name}}\",\n        arguments: { \u002F* input *\u002F },\n      });\n      \u002F\u002F handle result\n    });\n\n    app.connect().then(() => {\n      const hostContext = app.getHostContext();\n      if (hostContext) applyHostContext(hostContext);\n    });\n  \u003C\u002Fscript>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>`;\n\nexport const {{RESOURCE_EXPORT}} = appResource('ui:\u002F\u002F{{tool-name}}\u002Fapp.html', {\n  name: '{{tool-name}}-ui',\n  title: '{{TOOL_TITLE}} UI',\n  description: 'Interactive HTML app for {{tool_name}}.',\n  params: ParamsSchema,\n  \u002F\u002F auth: ['resource:{{tool-name}}-ui:read'],\n  _meta: {\n    ui: {\n      csp: { resourceDomains: ['https:\u002F\u002Funpkg.com'] },\n    },\n  },\n\n  handler(_params, ctx) {\n    ctx.log.debug('Serving app UI.', { resourceUri: ctx.uri?.href });\n    return APP_HTML;\n  },\n});\n```\n\nNo `list` callback: `ui:\u002F\u002F{{tool-name}}\u002Fapp.html` has no template variables, so it registers as a fixed resource that `resources\u002Flist` already reports. Registration ignores `list` on a variable-free URI.\n\n## UI Notes\n\n- **Ship self-contained HTML.** Author with Vite + `vite-plugin-singlefile` or inline the SDK. Live CDN imports in a `ui:\u002F\u002F` resource are a CSP footgun (every domain has to be whitelisted on `_meta.ui.csp.resourceDomains`), a supply-chain footgun (third-party JS executes inside the host's iframe), and a runtime footgun (every render needs network). The `unpkg` line in the template is for prototyping only.\n- **CSP.** MCP Apps iframes run under deny-by-default CSP. With `appResource()`, put `_meta.ui.csp.resourceDomains` on the definition; the builder mirrors it into returned `resources\u002Fread` content items. With plain `resource()`, attach `_meta.ui` yourself in `format()`.\n- **Adopt the host's visual identity, don't impose your own.** App UIs render inside the host's iframe alongside its native UI. Three host hooks layer on top of your CSS:\n  - `applyDocumentTheme(hostContext.theme)` — sets `color-scheme` and a `data-theme` attribute on `\u003Chtml>`\n  - `applyHostStyleVariables(hostContext.styles.variables)` — installs host CSS custom properties on `:root`. The MCP Apps spec fixes the names (`McpUiStyleVariableKey`): `--color-background-primary`, `--color-text-primary`, `--color-border-primary`, `--font-sans`, `--border-radius-md`, and so on. A host may send any subset.\n  - `applyHostFonts(hostContext.styles.css.fonts)` — installs `@font-face` rules for the host's font stack\n\n  Author CSS to *consume* these via `var(--color-background-primary, var(--bg))` — the host variable first, a local token as the fallback. Don't hardcode brand colors that fight the host.\n- **Pre-connect baseline.** `app.connect()` is async — host context arrives a frame or two after first paint. Without a baseline, the UI flashes unstyled or wrong-themed on light hosts. Ship local tokens with a light default, a `prefers-color-scheme` dark override, and a `data-theme` pin so the first frame is sensible:\n\n  ```css\n  :root { color-scheme: light dark; --bg: #fff; --fg: #111; }\n  @media (prefers-color-scheme: dark) { :root:not([data-theme=\"light\"]) { --bg: #0c0d12; --fg: #ededef; } }\n  :root[data-theme=\"dark\"] { --bg: #0c0d12; --fg: #ededef; }\n  body { background: var(--color-background-primary, var(--bg)); color: var(--color-text-primary, var(--fg)); }\n  ```\n\n  Once host context is applied, the host's variables win; the local tokens cover the gap before it arrives and any variable the host omits.\n- **`format()` for app tools.** The first `text` content block is typically JSON that the UI parses via `ontoolresult`. Additional blocks are the human-readable fallback that non-app hosts and LLMs consume — they must render every field the LLM needs to reason about. JSON-only payloads leave model-visible context blind.\n- **App resource `format()`.** `appResource()` already preserves raw HTML for the default app MIME type and mirrors definition `_meta.ui` into content items. Add a custom `format()` only when you need extra per-read metadata or non-default content shaping.\n\n## Registration\n\n```typescript\n\u002F\u002F src\u002Findex.ts (fresh scaffold default)\nimport { createApp } from '@cyanheads\u002Fmcp-ts-core';\nimport { {{TOOL_EXPORT}} } from '.\u002Fmcp-server\u002Ftools\u002Fdefinitions\u002F{{tool-name}}.app-tool.js';\nimport { {{RESOURCE_EXPORT}} } from '.\u002Fmcp-server\u002Fresources\u002Fdefinitions\u002F{{tool-name}}-ui.app-resource.js';\n\nawait createApp({\n  tools: [{{TOOL_EXPORT}}],\n  resources: [{{RESOURCE_EXPORT}}],\n  prompts: [\u002F* existing prompts *\u002F],\n});\n```\n\nIf the repo already uses `definitions\u002Findex.ts` barrels, update those instead of changing the registration pattern.\n\n## Checklist\n\n- [ ] App tool created at `src\u002Fmcp-server\u002Ftools\u002Fdefinitions\u002F{{tool-name}}.app-tool.ts` using `appTool()`\n- [ ] App resource created at `src\u002Fmcp-server\u002Fresources\u002Fdefinitions\u002F{{tool-name}}-ui.app-resource.ts` using `appResource()`\n- [ ] `resourceUri` matches between tool and resource (`ui:\u002F\u002F{{tool-name}}\u002Fapp.html`)\n- [ ] Zod schemas: all fields have `.describe()`, only JSON-Schema-serializable types\n- [ ] `format()` first block is `JSON.stringify(result)` — the full output object for the UI to parse via `app.ontoolresult`. Subsequent blocks are human-readable, content-complete fallback for non-app hosts and LLMs\n- [ ] App resource `_meta.ui.csp.resourceDomains` lists every external domain loaded by the UI\n- [ ] UI bundles or inlines the client SDK for the shipped HTML, and handles `app.ontoolresult`\n- [ ] UI applies host context updates via `app.onhostcontextchanged`\n- [ ] UI CSS reads host variables with local fallbacks (`var(--color-background-primary, var(--bg))`) over a light\u002Fdark baseline\n- [ ] No `list` callback on the fixed `ui:\u002F\u002F…\u002Fapp.html` resource — `resources\u002Flist` already reports it\n- [ ] Both registered in the project's existing `createApp()` arrays (directly or via barrels)\n- [ ] Handler tested directly via `createMockContext()`, or `add-test` skill run to scaffold the test file\n- [ ] `bun run devcheck` passes (linter validates `_meta.ui` and tool\u002Fresource pairing)\n- [ ] Smoke-tested with `bun run rebuild && bun run start:stdio \u003C \u002Fdev\u002Fnull` (or `start:http`); the `Core services constructed` record lists the tool in `tools` and its UI resource in `resources`\n- [ ] View rendered in the headless MCP Apps host (`mcp-ts-core app-render`; `field-test` skill, Step 6): `initialized` is true, `errors` and `cspViolations` are empty, and the screenshot shows the tool result\n",{"name":5,"metadata":47,"description":51},{"type":34,"author":48,"version":49,"audience":50},"cyanheads","1.8","external","Scaffold an MCP App tool + UI resource pair. Use when the user asks to add a tool with interactive UI, create an MCP App, or build a visual\u002Finteractive tool.\n","main","3df1d0d67b30ff15236561f8c0f9f06d33f42dfd","skills\u002Fmcp-servers\u002Fcyanheads-add-app-tool",17,"https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia","https:\u002F\u002Feth2030.com","Hardcore infra. Building @alt-research and applied cryptography.","2026-11-11 08:47:40",{"source":8,"skillName":5,"commands":62,"prompt":72,"zipAvailable":73},[63,66,69],{"tool":64,"command":65},"npx","npx skills add jiayaoqijia\u002Fcryptoskill --skill add-app-tool",{"tool":67,"command":68},"bunx","bunx skills add jiayaoqijia\u002Fcryptoskill --skill add-app-tool",{"tool":70,"command":71},"pnpm","pnpm dlx skills add jiayaoqijia\u002Fcryptoskill --skill add-app-tool","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fmcp-servers\u002Fcyanheads-add-app-tool\u002FSKILL.md",true,[75,79,85],{"path":76,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":78,"sourceHtmlUrl":16},"LICENSE","许可证",11317,{"path":80,"resourceType":81,"resourceTypeDesc":82,"sizeBytes":83,"sourceHtmlUrl":84},"SOURCE.md","REFERENCE","参考文档",371,"https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fmcp-servers\u002Fcyanheads-add-app-tool\u002FSOURCE.md",{"path":86,"resourceType":81,"resourceTypeDesc":82,"sizeBytes":87,"sourceHtmlUrl":88},"TRUST.auto.yaml",2191,"https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fmcp-servers\u002Fcyanheads-add-app-tool\u002FTRUST.auto.yaml",[90,118,140,158,171,181],{"skillNo":91,"name":92,"nameZh":6,"description":93,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":94,"licenseFileUrl":95,"sourceHtmlUrl":96,"categories":97,"keywords":98,"publishedAt":42},"01M712C436EQYE43342Q4E7GG1","recipe-performance","Diagnose a specific MetaMask user flow's real-app performance and Sentry critical-user-flow telemetry with deterministic mm-harness recipes. Use for CUF boundaries, cold\u002Fwarm\u002Fbackground-resume behavior, latency regressions, lifecycle tags, duplicate or blocking network requests, long tasks, layout\u002Fpaint cost, screen redraws, React rerenders, or browser trace analysis where tests alone cannot prove the real app.","MIT","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fwallets\u002Fmetamask-official-recipe-performance\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fwallets\u002Fmetamask-official-recipe-performance\u002FSKILL.md",[],[99,100,101,102,103,104,28,105,106,107,108,109,110,111,112,113,114,115,116,117],"performance","recipe performance","recipe","mm","sentry","harness","deterministic","recipes","flow","cold","critical","analysis","tasks","react","browser","layout","screen","network","metamask",{"skillNo":119,"name":120,"nameZh":6,"description":121,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":122,"sourceHtmlUrl":123,"categories":124,"keywords":125,"publishedAt":42},"01M70XT2F8A5CQZ81JBP98KSW3","mexc-spot-websocket","MEXC Spot WebSocket workflows for public streams, protobuf handling, depth maintenance, listen keys, and private streams without exposing API keys.","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-spot-websocket\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-spot-websocket\u002FSKILL.md",[],[126,127,128,129,130,131,132,133,134,135,136,137,138,139],"mexc spot websocket","spot","websocket","api","workflows","public","listen","streams","depth","keys","for","api keys","maintenance","protobuf",{"skillNo":141,"name":142,"nameZh":6,"description":143,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":144,"sourceHtmlUrl":145,"categories":146,"keywords":147,"publishedAt":42},"01M70XT0MBXN3WZY1BKN0CPNKR","mexc-spot-rest","MEXC Spot REST workflows for public data, signed account requests, orders, wallet\u002Fadmin actions, and secure signing without exposing API keys.","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-spot-rest\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-spot-rest\u002FSKILL.md",[],[148,127,149,150,151,152,153,137,136,135,154,155,156,131,157,130,129],"mexc spot rest","REST","admin","signing","Wallet","requests","actions","secure","data","orders",{"skillNo":159,"name":160,"nameZh":6,"description":161,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":162,"sourceHtmlUrl":163,"categories":164,"keywords":165,"publishedAt":42},"01M70XSYSVZ2N0N91XCAR2CAK0","mexc-futures-websocket","MEXC Futures WebSocket workflows for public streams, private account\u002Forder\u002Fposition streams, ping\u002Fpong, subscriptions, and depth maintenance.","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-futures-websocket\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-futures-websocket\u002FSKILL.md",[],[166,167,128,168,169,138,136,170,134,133,131,130],"mexc futures websocket","futures","position","ping","subscriptions",{"skillNo":172,"name":173,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":94,"licenseFileUrl":174,"sourceHtmlUrl":175,"categories":176,"keywords":177,"publishedAt":42},"01M6H0ZWHVE5W04WG2DKQE2SQC","0xklen-version-and-refresh-sanctions-lists","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fpayments\u002F0xklen-version-and-refresh-sanctions-lists\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fpayments\u002F0xklen-version-and-refresh-sanctions-lists\u002FSKILL.md",[],[178,179,180],"refresh","0xklen version and refresh sanctions lists","version",{"skillNo":182,"name":183,"nameZh":6,"description":184,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":94,"licenseFileUrl":185,"sourceHtmlUrl":186,"categories":187,"keywords":188,"publishedAt":42},"01M6H0Z28Q3R8J0S799GJQWXM3","verify-tests-cover-changed-lines","Use when a PR adds code that the existing suite may not exercise. Measures patch coverage — the share of newly changed lines hit by tests — before approving.","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fpayments\u002F0xklen-verify-tests-cover-changed-lines\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fpayments\u002F0xklen-verify-tests-cover-changed-lines\u002FSKILL.md",[],[189,190,191,192,193,194,195,29,196,197,198],"tests","cover","verify tests cover changed lines","verify","coverage","pr","exercise","patch","code","share"]