Back to skills
SkillHub ClubShip Full StackFull StackFrontendBackend

mcp-app-builder

Build new MCP Apps (MCP servers with React UI output) using @modelcontextprotocol/ext-apps and the MCP SDK. Use when asked to scaffold or implement MCP App servers, add UI-rendering tools/resources, or migrate a standard MCP server to an MCP App with Vite single-file UI bundles.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3,114
Hot score
99
Updated
March 20, 2026
Overall rating
C4.6
Composite score
4.6
Best-practice grade
B71.9

Install command

npx @skill-hub/cli install openclaw-skills-mcp-app-builder

Repository

openclaw/skills

Skill path: skills/hollaugo/mcp-app-builder

Build new MCP Apps (MCP servers with React UI output) using @modelcontextprotocol/ext-apps and the MCP SDK. Use when asked to scaffold or implement MCP App servers, add UI-rendering tools/resources, or migrate a standard MCP server to an MCP App with Vite single-file UI bundles.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack, Frontend, Backend, Integration.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: openclaw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install mcp-app-builder into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/openclaw/skills before adding mcp-app-builder to shared team environments
  • Use mcp-app-builder for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: mcp-app-builder
description: Build new MCP Apps (MCP servers with React UI output) using @modelcontextprotocol/ext-apps and the MCP SDK. Use when asked to scaffold or implement MCP App servers, add UI-rendering tools/resources, or migrate a standard MCP server to an MCP App with Vite single-file UI bundles.
---

# MCP App Builder

## Overview
Create MCP Apps that expose tools with visual React UIs for ChatGPT or Claude. Follow the exact dependency versions and server/UI patterns in `references/mcp-app-spec.md`.

## Workflow
1. Clarify requirements: what data to visualize, UI pattern (card, table, chart, dashboard, form), data source, and how many tools (start with 1-2).
2. Design tools and UI mapping: define tool names, zod input schemas, output shape, and UI resource URIs (`ui://.../app.html`). Map each tool to one React entrypoint and one HTML file.
3. Scaffold the project: start from `assets/mcp-app-template/` when possible, then customize tool names, schemas, and UI. Ensure `package.json` uses the exact versions, plus `tsconfig.json`, `vite.config.ts`, Tailwind + PostCSS, and per-tool build scripts.
4. Implement the server: use `registerAppTool`/`registerAppResource`, zod schemas directly, `createServer()` factory per request, and `createMcpExpressApp` with `app.all("/mcp", ...)`.
5. Implement the UI: use `useApp` + `useHostStyles`, parse tool results, handle loading/error/empty states, and apply safe-area insets.
6. Build and test: run `npm run build`, then `npm run serve`, then verify via a tunnel if needed.

## Hard Requirements
- Use the exact dependency versions listed in `references/mcp-app-spec.md`.
- Use `registerAppTool`/`registerAppResource` and zod schemas directly (not JSON Schema objects).
- Create a new `McpServer` instance per request via `createServer()`.
- Use `createMcpExpressApp` and `app.all("/mcp", ...)`.
- Bundle UI into single-file HTML via `vite-plugin-singlefile`.
- Use host CSS variables for theme compatibility.

## References
- `references/mcp-app-spec.md` (authoritative spec, patterns, code templates, gotchas)
## Assets
- `assets/mcp-app-template/` (ready-to-copy MCP App skeleton with one tool + UI)


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### references/mcp-app-spec.md

```markdown
# MCP App Specification (Authoritative)

This reference defines the required dependencies, server/UI patterns, and build workflow for MCP Apps.

## Critical Dependencies (Exact Versions Required)

```json
{
  "dependencies": {
    "@modelcontextprotocol/ext-apps": "^1.0.0",
    "@modelcontextprotocol/sdk": "^1.24.0",
    "zod": "^4.1.13",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "cors": "^2.8.5",
    "express": "^5.1.0"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^4.3.4",
    "vite": "^6.0.0",
    "vite-plugin-singlefile": "^2.3.0",
    "tailwindcss": "^3.4.17",
    "autoprefixer": "^10.4.20",
    "postcss": "^8.4.49",
    "tsx": "^4.19.0",
    "typescript": "^5.9.3"
  }
}
```

## Key Differences from Standard MCP Servers

- UI Output: Text/JSON only -> Visual React components
- Package: `@modelcontextprotocol/sdk` only -> also `@modelcontextprotocol/ext-apps`
- Schema: JSON Schema objects -> Zod schemas directly
- Tool registration: `server.tool()` -> `registerAppTool()`
- Resource registration: `server.resource()` -> `registerAppResource()`
- Server pattern: single instance -> `createServer()` factory per request
- Express setup: manual -> `createMcpExpressApp()` helper
- HTTP handler: `app.post("/mcp", ...)` -> `app.all("/mcp", ...)`

## Workflow Summary

### Phase 1: Understand the App
Ask:
- What data will this visualize?
- What UI pattern (card, chart, table, dashboard, form)?
- What API/data source (REST API, database, generated data)?
- How many tools (start with 1-2)?

### Phase 2: Design Tools With UIs
Each tool needs:
- Tool definition with Zod input schema
- Visual React component for rendering results
- Resource URI linking tool to UI

Example mapping:
- Tool: `get-stock-detail`
- Input: `{ symbol: z.string() }`
- Resource URI: `ui://stock-detail/app.html`
- Component: `StockDetailCard.tsx`

### Phase 3: Project Structure

```
{app-name}/
├── server.ts
├── package.json
├── vite.config.ts
├── tsconfig.json
├── tailwind.config.js
├── postcss.config.js
├── .gitignore
├── src/
│   ├── index.css
│   ├── {tool-name}.tsx
│   └── components/
└── {tool-name}.html
```

### Phase 4: Server Implementation (Required Pattern)

```ts
import {
  registerAppTool,
  registerAppResource,
  RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
import cors from "cors";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";

// Works both from source (server.ts) and compiled (dist/server.js)
const DIST_DIR = import.meta.filename.endsWith(".ts")
  ? path.join(import.meta.dirname, "dist")
  : import.meta.dirname;

// Resource URIs
const toolUIResourceUri = "ui://tool-name/app.html";

// Server Factory - CRITICAL: New server per request
export function createServer(): McpServer {
  const server = new McpServer({
    name: "App Name",
    version: "1.0.0",
  });

  // Register tool with zod schema (NOT JSON Schema)
  registerAppTool(
    server,
    "tool-name",
    {
      title: "Tool Title",
      description: "When to use this tool...",
      inputSchema: {
        param: z.string().describe("Parameter description"),
      },
      _meta: { ui: { resourceUri: toolUIResourceUri } },
    },
    async ({ param }): Promise<CallToolResult> => {
      const result = await fetchData(param);
      return {
        content: [{ type: "text", text: JSON.stringify(result) }],
        structuredContent: result,
      };
    }
  );

  // Register UI resource
  registerAppResource(
    server,
    toolUIResourceUri,
    toolUIResourceUri,
    { mimeType: RESOURCE_MIME_TYPE },
    async (): Promise<ReadResourceResult> => {
      const html = await fs.readFile(
        path.join(DIST_DIR, "tool-name", "tool-name.html"),
        "utf-8"
      );
      return {
        contents: [
          {
            uri: toolUIResourceUri,
            mimeType: RESOURCE_MIME_TYPE,
            text: html,
          },
        ],
      };
    }
  );

  return server;
}

// HTTP Server - MUST use createMcpExpressApp and app.all
const port = parseInt(process.env.PORT ?? "3001", 10);
const app = createMcpExpressApp({ host: "0.0.0.0" });
app.use(cors());

app.all("/mcp", async (req, res) => {
  const server = createServer();
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined,
  });

  res.on("close", () => {
    transport.close().catch(() => {});
    server.close().catch(() => {});
  });

  try {
    await server.connect(transport);
    await transport.handleRequest(req, res, req.body);
  } catch (error) {
    console.error("MCP error:", error);
    if (!res.headersSent) {
      res.status(500).json({
        jsonrpc: "2.0",
        error: { code: -32603, message: "Internal server error" },
        id: null,
      });
    }
  }
});

app.listen(port, () => {
  console.log(`Server listening on http://localhost:${port}/mcp`);
});
```

### Phase 5: React UI Implementation (Required Pattern)

```tsx
import { StrictMode, useState, useEffect } from "react";
import { createRoot } from "react-dom/client";
import { useApp, useHostStyles } from "@modelcontextprotocol/ext-apps/react";
import "./index.css";

interface ToolData {
  // Define based on tool output
}

function ToolUI() {
  const [data, setData] = useState<ToolData | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);

  const { app } = useApp({
    appInfo: { name: "Tool Name", version: "1.0.0" },
    onAppCreated: (app) => {
      app.ontoolresult = (result) => {
        setLoading(false);
        const text = result.content?.find((c) => c.type === "text")?.text;
        if (text) {
          try {
            const parsed = JSON.parse(text);
            if (parsed.error) {
              setError(parsed.message);
            } else {
              setData(parsed);
            }
          } catch {
            setError("Failed to parse data");
          }
        }
      };
    },
  });

  // Apply host CSS variables for theme integration
  useHostStyles(app);

  // Handle safe area insets for mobile
  useEffect(() => {
    if (!app) return;
    app.onhostcontextchanged = (ctx) => {
      if (ctx.safeAreaInsets) {
        const { top, right, bottom, left } = ctx.safeAreaInsets;
        document.body.style.padding = `${top}px ${right}px ${bottom}px ${left}px`;
      }
    };
  }, [app]);

  if (loading) return <LoadingSkeleton />;
  if (error) return <ErrorDisplay message={error} />;
  if (!data) return <EmptyState />;

  return <DataVisualization data={data} />;
}

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <ToolUI />
  </StrictMode>
);
```

### Phase 6: Host CSS Variable Integration

```css
@tailwind base;
@tailwind components;
@tailwind utilities;

body {
  margin: 0;
  padding: 0;
  font-family: var(--font-sans, system-ui, -apple-system, sans-serif);
  background: var(--color-background-primary, #ffffff);
  color: var(--color-text-primary, #1a1a1a);
}

.card {
  background: var(--color-background-primary, #ffffff);
  border: 1px solid var(--color-border-primary, #e5e5e5);
  border-radius: var(--border-radius-lg, 12px);
  padding: 1.5rem;
}

.card-inner {
  background: var(--color-background-secondary, #f5f5f5);
  border-radius: var(--border-radius-md, 8px);
  padding: 1rem;
}

.text-secondary {
  color: var(--color-text-secondary, #525252);
}

.text-tertiary {
  color: var(--color-text-tertiary, #737373);
}
```

### Phase 7: Vite Single-File Bundling

```ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { viteSingleFile } from "vite-plugin-singlefile";

export default defineConfig({
  plugins: [react(), viteSingleFile()],
  build: {
    outDir: "dist",
    rollupOptions: {
      input: process.env.INPUT,
    },
  },
});
```

Build scripts (one entry point per tool):

```json
{
  "scripts": {
    "build": "npm run build:tool1 && npm run build:tool2",
    "build:tool1": "INPUT=tool1.html vite build --outDir dist/tool1",
    "build:tool2": "INPUT=tool2.html vite build --outDir dist/tool2",
    "serve": "tsx server.ts",
    "dev": "npm run build && npm run serve"
  }
}
```

## Common Gotchas

- `zod@^4.1.13` is required. Older versions cause `v3Schema.safeParseAsync is not a function` errors.
- `inputSchema` uses Zod directly, not JSON Schema objects.
- Handler args are destructured: `async ({ symbol })` not `async (args)`.
- Use `app.all` (not `app.post`).
- `createServer()` factory is required per request.
- Use `createMcpExpressApp`, not manual Express setup.
- UI must be bundled into a single HTML file with `viteSingleFile`.
- Import `RESOURCE_MIME_TYPE` from `@modelcontextprotocol/ext-apps/server`.
- Call `useHostStyles(app)` in the React UI.

## Testing

1. Build the UI: `npm run build`
2. Start the server: `npm run serve`
3. Expose locally: `npx cloudflared tunnel --url http://127.0.0.1:3001`
4. Connect to Claude/ChatGPT via the tunnel URL
5. Invoke the tool and verify UI renders

```



---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### _meta.json

```json
{
  "owner": "hollaugo",
  "slug": "mcp-app-builder",
  "displayName": "Mcp App Builder",
  "latest": {
    "version": "0.1.0",
    "publishedAt": 1772174926683,
    "commit": "https://github.com/openclaw/skills/commit/97760bf2c074a9ed0d3c7101e4d8d820e73ba4a6"
  },
  "history": []
}

```

### assets/mcp-app-template/package.json

```json
{
  "name": "mcp-app-template",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "npm run build:tool-name",
    "build:tool-name": "INPUT=tool-name.html vite build --outDir dist/tool-name",
    "serve": "tsx server.ts",
    "dev": "npm run build && npm run serve"
  },
  "dependencies": {
    "@modelcontextprotocol/ext-apps": "^1.0.0",
    "@modelcontextprotocol/sdk": "^1.24.0",
    "zod": "^4.1.13",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "cors": "^2.8.5",
    "express": "^5.1.0"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^4.3.4",
    "vite": "^6.0.0",
    "vite-plugin-singlefile": "^2.3.0",
    "tailwindcss": "^3.4.17",
    "autoprefixer": "^10.4.20",
    "postcss": "^8.4.49",
    "tsx": "^4.19.0",
    "typescript": "^5.9.3"
  }
}

```

### assets/mcp-app-template/postcss.config.js

```javascript
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

```

### assets/mcp-app-template/server.ts

```typescript
import {
  registerAppTool,
  registerAppResource,
  RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
import cors from "cors";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";

// Works both from source (server.ts) and compiled (dist/server.js)
const DIST_DIR = import.meta.filename.endsWith(".ts")
  ? path.join(import.meta.dirname, "dist")
  : import.meta.dirname;

const TOOL_NAME = "tool-name";
const toolUIResourceUri = `ui://${TOOL_NAME}/app.html`;

async function fetchData(param: string) {
  return {
    param,
    generatedAt: new Date().toISOString(),
    score: Math.max(1, param.length * 7),
    notes: `Sample data for ${param}`,
  };
}

// Server Factory - CRITICAL: New server per request
export function createServer(): McpServer {
  const server = new McpServer({
    name: "MCP App Template",
    version: "1.0.0",
  });

  registerAppTool(
    server,
    TOOL_NAME,
    {
      title: "Tool Name",
      description: "Describe when to use this tool.",
      inputSchema: {
        param: z.string().describe("Parameter description"),
      },
      _meta: { ui: { resourceUri: toolUIResourceUri } },
    },
    async ({ param }): Promise<CallToolResult> => {
      const result = await fetchData(param);
      return {
        content: [{ type: "text", text: JSON.stringify(result) }],
        structuredContent: result,
      };
    }
  );

  registerAppResource(
    server,
    toolUIResourceUri,
    toolUIResourceUri,
    { mimeType: RESOURCE_MIME_TYPE },
    async (): Promise<ReadResourceResult> => {
      const html = await fs.readFile(
        path.join(DIST_DIR, TOOL_NAME, `${TOOL_NAME}.html`),
        "utf-8"
      );
      return {
        contents: [
          {
            uri: toolUIResourceUri,
            mimeType: RESOURCE_MIME_TYPE,
            text: html,
          },
        ],
      };
    }
  );

  return server;
}

// HTTP Server - MUST use createMcpExpressApp and app.all
const port = parseInt(process.env.PORT ?? "3001", 10);
const app = createMcpExpressApp({ host: "0.0.0.0" });
app.use(cors());

app.all("/mcp", async (req, res) => {
  const server = createServer();
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined,
  });

  res.on("close", () => {
    transport.close().catch(() => {});
    server.close().catch(() => {});
  });

  try {
    await server.connect(transport);
    await transport.handleRequest(req, res, req.body);
  } catch (error) {
    console.error("MCP error:", error);
    if (!res.headersSent) {
      res.status(500).json({
        jsonrpc: "2.0",
        error: { code: -32603, message: "Internal server error" },
        id: null,
      });
    }
  }
});

app.listen(port, () => {
  console.log(`Server listening on http://localhost:${port}/mcp`);
});

```

### assets/mcp-app-template/src/index.css

```css
@tailwind base;
@tailwind components;
@tailwind utilities;

body {
  margin: 0;
  padding: 0;
  font-family: var(--font-sans, system-ui, -apple-system, sans-serif);
  background: var(--color-background-primary, #ffffff);
  color: var(--color-text-primary, #1a1a1a);
}

.card {
  background: var(--color-background-primary, #ffffff);
  border: 1px solid var(--color-border-primary, #e5e5e5);
  border-radius: var(--border-radius-lg, 12px);
  padding: 1.5rem;
}

.card-inner {
  background: var(--color-background-secondary, #f5f5f5);
  border-radius: var(--border-radius-md, 8px);
  padding: 1rem;
}

.text-secondary {
  color: var(--color-text-secondary, #525252);
}

.text-tertiary {
  color: var(--color-text-tertiary, #737373);
}

```

### assets/mcp-app-template/src/tool-name.tsx

```tsx
import { StrictMode, useEffect, useMemo, useState } from "react";
import { createRoot } from "react-dom/client";
import { useApp, useHostStyles } from "@modelcontextprotocol/ext-apps/react";
import { Card } from "./components/Card";
import "./index.css";

interface ToolData {
  param: string;
  generatedAt: string;
  score: number;
  notes: string;
}

function LoadingSkeleton() {
  return (
    <div className="min-h-screen p-4">
      <Card>
        <div className="h-5 w-40 animate-pulse rounded bg-gray-200" />
        <div className="mt-4 space-y-2">
          <div className="h-4 w-full animate-pulse rounded bg-gray-200" />
          <div className="h-4 w-3/4 animate-pulse rounded bg-gray-200" />
        </div>
      </Card>
    </div>
  );
}

function ErrorDisplay({ message }: { message: string }) {
  return (
    <div className="min-h-screen p-4">
      <Card className="border-red-200">
        <div className="text-sm font-semibold text-red-600">Error</div>
        <div className="mt-2 text-sm text-secondary">{message}</div>
      </Card>
    </div>
  );
}

function EmptyState() {
  return (
    <div className="min-h-screen p-4">
      <Card>
        <div className="text-sm font-semibold">No data yet</div>
        <div className="mt-2 text-sm text-secondary">
          Invoke the tool to see results here.
        </div>
      </Card>
    </div>
  );
}

function DataVisualization({ data }: { data: ToolData }) {
  const formattedDate = useMemo(() => {
    try {
      return new Date(data.generatedAt).toLocaleString();
    } catch {
      return data.generatedAt;
    }
  }, [data.generatedAt]);

  return (
    <div className="min-h-screen p-4">
      <Card>
        <div className="flex items-center justify-between">
          <div>
            <div className="text-lg font-semibold">Tool Result</div>
            <div className="text-xs text-tertiary">{formattedDate}</div>
          </div>
          <div className="text-sm font-semibold">Score: {data.score}</div>
        </div>
        <div className="mt-4 card-inner">
          <div className="text-sm text-secondary">Parameter</div>
          <div className="text-base font-medium">{data.param}</div>
        </div>
        <div className="mt-4 text-sm text-secondary">{data.notes}</div>
      </Card>
    </div>
  );
}

function ToolUI() {
  const [data, setData] = useState<ToolData | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);

  const { app } = useApp({
    appInfo: { name: "Tool Name", version: "1.0.0" },
    onAppCreated: (app) => {
      app.ontoolresult = (result) => {
        setLoading(false);
        const text = result.content?.find((c) => c.type === "text")?.text;
        if (!text) {
          setError("No data returned by tool.");
          return;
        }
        try {
          const parsed = JSON.parse(text) as ToolData | { error?: string; message?: string };
          if (parsed && typeof parsed === "object" && "error" in parsed) {
            setError(parsed.message ?? "Tool returned an error.");
            return;
          }
          setData(parsed as ToolData);
        } catch {
          setError("Failed to parse data");
        }
      };
    },
  });

  useHostStyles(app);

  useEffect(() => {
    if (!app) return;
    app.onhostcontextchanged = (ctx) => {
      if (ctx.safeAreaInsets) {
        const { top, right, bottom, left } = ctx.safeAreaInsets;
        document.body.style.padding = `${top}px ${right}px ${bottom}px ${left}px`;
      }
    };
  }, [app]);

  if (loading) return <LoadingSkeleton />;
  if (error) return <ErrorDisplay message={error} />;
  if (!data) return <EmptyState />;

  return <DataVisualization data={data} />;
}

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <ToolUI />
  </StrictMode>
);

```

### assets/mcp-app-template/tailwind.config.js

```javascript
/** @type {import('tailwindcss').Config} */
export default {
  content: ["./src/**/*.{ts,tsx}", "./tool-name.html"],
  theme: {
    extend: {},
  },
  plugins: [],
};

```

### assets/mcp-app-template/tool-name.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Tool Name</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/tool-name.tsx"></script>
  </body>
</html>

```

### assets/mcp-app-template/tsconfig.json

```json
{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "jsx": "react-jsx",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "types": ["node"]
  },
  "include": ["src", "server.ts", "vite.config.ts", "tool-name.html"]
}

```

### assets/mcp-app-template/vite.config.ts

```typescript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { viteSingleFile } from "vite-plugin-singlefile";

export default defineConfig({
  plugins: [react(), viteSingleFile()],
  build: {
    outDir: "dist",
    rollupOptions: {
      input: process.env.INPUT,
    },
  },
});

```

mcp-app-builder | SkillHub