From 9833096dbf0d6318e2fd986d6158d363268a68f6 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Sun, 25 Jan 2026 22:36:01 +0200 Subject: [PATCH 1/2] feat: add @modelcontextprotocol/create-mcp-app package Enable scaffolding new MCP App projects via `npm create @modelcontextprotocol/mcp-app`. Features: - Interactive CLI with @clack/prompts for beautiful UX - React and Vanilla JS templates included - Uses tsx for broader compatibility (no bun dependency) - Templates include server, UI, and build configuration - Supports --template and --no-install flags Changes: - Add packages/create-mcp-app/ with CLI and templates - Add packages/* to workspaces in root package.json - Add publish-packages job to npm-publish workflow Co-Authored-By: Claude Opus 4.5 --- .github/workflows/npm-publish.yml | 33 ++++ package-lock.json | 77 ++++++--- package.json | 3 +- packages/create-mcp-app/README.md | 54 ++++++ packages/create-mcp-app/package.json | 31 ++++ packages/create-mcp-app/src/cli.ts | 155 ++++++++++++++++++ packages/create-mcp-app/src/index.ts | 7 + packages/create-mcp-app/src/scaffold.ts | 88 ++++++++++ packages/create-mcp-app/src/utils.ts | 53 ++++++ .../create-mcp-app/templates/base/.gitignore | 2 + .../create-mcp-app/templates/base/main.ts | 88 ++++++++++ .../templates/base/scripts/bundle-server.mjs | 48 ++++++ .../create-mcp-app/templates/base/server.ts | 69 ++++++++ .../templates/base/src/global.css | 18 ++ .../templates/base/tsconfig.server.json | 17 ++ .../templates/react/mcp-app.html | 14 ++ .../templates/react/package.json.tmpl | 37 +++++ .../templates/react/src/mcp-app.module.css | 57 +++++++ .../templates/react/src/mcp-app.tsx | 122 ++++++++++++++ .../templates/react/src/vite-env.d.ts | 1 + .../templates/react/tsconfig.json | 20 +++ .../templates/react/vite.config.ts | 25 +++ .../templates/vanillajs/mcp-app.html | 22 +++ .../templates/vanillajs/package.json.tmpl | 32 ++++ .../templates/vanillajs/src/mcp-app.css | 57 +++++++ .../templates/vanillajs/src/mcp-app.ts | 89 ++++++++++ .../templates/vanillajs/tsconfig.json | 19 +++ .../templates/vanillajs/vite.config.ts | 24 +++ packages/create-mcp-app/tsconfig.json | 16 ++ 29 files changed, 1253 insertions(+), 25 deletions(-) create mode 100644 packages/create-mcp-app/README.md create mode 100644 packages/create-mcp-app/package.json create mode 100644 packages/create-mcp-app/src/cli.ts create mode 100644 packages/create-mcp-app/src/index.ts create mode 100644 packages/create-mcp-app/src/scaffold.ts create mode 100644 packages/create-mcp-app/src/utils.ts create mode 100644 packages/create-mcp-app/templates/base/.gitignore create mode 100644 packages/create-mcp-app/templates/base/main.ts create mode 100644 packages/create-mcp-app/templates/base/scripts/bundle-server.mjs create mode 100644 packages/create-mcp-app/templates/base/server.ts create mode 100644 packages/create-mcp-app/templates/base/src/global.css create mode 100644 packages/create-mcp-app/templates/base/tsconfig.server.json create mode 100644 packages/create-mcp-app/templates/react/mcp-app.html create mode 100644 packages/create-mcp-app/templates/react/package.json.tmpl create mode 100644 packages/create-mcp-app/templates/react/src/mcp-app.module.css create mode 100644 packages/create-mcp-app/templates/react/src/mcp-app.tsx create mode 100644 packages/create-mcp-app/templates/react/src/vite-env.d.ts create mode 100644 packages/create-mcp-app/templates/react/tsconfig.json create mode 100644 packages/create-mcp-app/templates/react/vite.config.ts create mode 100644 packages/create-mcp-app/templates/vanillajs/mcp-app.html create mode 100644 packages/create-mcp-app/templates/vanillajs/package.json.tmpl create mode 100644 packages/create-mcp-app/templates/vanillajs/src/mcp-app.css create mode 100644 packages/create-mcp-app/templates/vanillajs/src/mcp-app.ts create mode 100644 packages/create-mcp-app/templates/vanillajs/tsconfig.json create mode 100644 packages/create-mcp-app/templates/vanillajs/vite.config.ts create mode 100644 packages/create-mcp-app/tsconfig.json diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 3b237cbe1..9b4086144 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -137,3 +137,36 @@ jobs: run: npm publish --workspace examples/${{ matrix.example }} --provenance --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_SECRET }} + + publish-packages: + runs-on: ubuntu-latest + if: github.event_name == 'release' + environment: Release + needs: [publish] + + permissions: + contents: read + id-token: write + + strategy: + fail-fast: false + matrix: + package: + - create-mcp-app + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + registry-url: "https://registry.npmjs.org" + - run: npm ci + + - name: Build package + run: npm run build --workspace packages/${{ matrix.package }} + + - name: Publish package + run: npm publish --workspace packages/${{ matrix.package }} --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_SECRET }} diff --git a/package-lock.json b/package-lock.json index 89d82cfef..5ef689478 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "hasInstallScript": true, "license": "MIT", "workspaces": [ - "examples/*" + "examples/*", + "packages/*" ], "devDependencies": { "@boneskull/typedoc-plugin-mermaid": "^0.2.0", @@ -937,7 +938,6 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -2346,6 +2346,10 @@ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", "license": "MIT" }, + "node_modules/@modelcontextprotocol/create-mcp-app": { + "resolved": "packages/create-mcp-app", + "link": true + }, "node_modules/@modelcontextprotocol/ext-apps": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-0.4.1.tgz", @@ -2402,7 +2406,6 @@ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.3.tgz", "integrity": "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==", "license": "MIT", - "peer": true, "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", @@ -3482,7 +3485,6 @@ "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.1", @@ -3689,7 +3691,6 @@ "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3714,7 +3715,6 @@ "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4117,7 +4117,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4459,7 +4458,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5175,7 +5173,6 @@ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "dev": true, "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -5648,7 +5645,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -7198,7 +7194,6 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz", "integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -7408,7 +7403,6 @@ "integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -7530,7 +7524,6 @@ "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.0.tgz", "integrity": "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" } @@ -7770,7 +7763,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, "license": "MIT" }, "node_modules/slash": { @@ -7821,7 +7813,6 @@ "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.11.tgz", "integrity": "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", @@ -8000,7 +7991,6 @@ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.48.0.tgz", "integrity": "sha512-+NUe82VoFP1RQViZI/esojx70eazGF4u0O/9ucqZ4rPcOZD+n5EVp17uYsqwdzjUjZyTpGKunHbDziW6AIAVkQ==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -8328,7 +8318,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -9028,7 +9017,6 @@ "integrity": "sha512-x4xW77QC3i5DUFMBp0qjukOTnr/sSg+oEs86nB3LjDslvAmwe/PUGDWbe3GrIqt59oTqoXK5GRK9tAa0sYMiog==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@gerrit0/mini-shiki": "^3.17.0", "lunr": "^2.3.9", @@ -9092,7 +9080,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9186,7 +9173,6 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -9363,7 +9349,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -9482,7 +9467,6 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz", "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.27", "@vue/compiler-sfc": "3.5.27", @@ -9643,7 +9627,6 @@ "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "dev": true, "license": "ISC", - "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -9694,7 +9677,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -9707,6 +9689,53 @@ "peerDependencies": { "zod": "^3.25 || ^4" } + }, + "packages/create-mcp-app": { + "name": "@modelcontextprotocol/create-mcp-app", + "version": "0.4.1", + "license": "MIT", + "dependencies": { + "@clack/prompts": "^0.10.0", + "picocolors": "^1.1.0" + }, + "bin": { + "create-mcp-app": "dist/index.js" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.9.3" + } + }, + "packages/create-mcp-app/node_modules/@clack/core": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.4.2.tgz", + "integrity": "sha512-NYQfcEy8MWIxrT5Fj8nIVchfRFA26yYKJcvBS7WlUIlw2OmQOY9DhGGXMovyI5J5PpxrCPGkgUi207EBrjpBvg==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "packages/create-mcp-app/node_modules/@clack/prompts": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.10.1.tgz", + "integrity": "sha512-Q0T02vx8ZM9XSv9/Yde0jTmmBQufZhPJfYAg2XrrrxWWaZgq1rr8nU8Hv710BQ1dhoP8rtY7YUdpGej2Qza/cw==", + "license": "MIT", + "dependencies": { + "@clack/core": "0.4.2", + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "packages/create-mcp-app/node_modules/@types/node": { + "version": "22.19.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", + "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } } } } diff --git a/package.json b/package.json index 7d757cbf6..2434e38fb 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "dist" ], "workspaces": [ - "examples/*" + "examples/*", + "packages/*" ], "scripts": { "postinstall": "node scripts/setup-bun.mjs || echo 'setup-bun.mjs failed or not available'", diff --git a/packages/create-mcp-app/README.md b/packages/create-mcp-app/README.md new file mode 100644 index 000000000..c6f5f8cbc --- /dev/null +++ b/packages/create-mcp-app/README.md @@ -0,0 +1,54 @@ +# @modelcontextprotocol/create-mcp-app + +Scaffold new MCP App projects with one command. + +## Usage + +```bash +# Interactive mode +npm create @modelcontextprotocol/mcp-app + +# With project name +npm create @modelcontextprotocol/mcp-app my-app + +# With template +npm create @modelcontextprotocol/mcp-app my-app --template react + +# Skip npm install +npm create @modelcontextprotocol/mcp-app my-app --no-install +``` + +## Templates + +- **react** - React + Vite + TypeScript +- **vanillajs** - Vanilla JavaScript + Vite + TypeScript + +## What's Included + +Each generated project includes: + +- MCP server with a sample `get-time` tool +- Interactive UI that communicates with the host +- Vite build configuration for bundling the UI +- TypeScript configuration +- Development server with hot reload + +## Getting Started + +After creating your project: + +```bash +cd my-app +npm install # if you used --no-install +npm run dev +``` + +Then test with the basic-host: + +```bash +SERVERS='["http://localhost:3001/mcp"]' npx @modelcontextprotocol/basic-host +``` + +## License + +MIT diff --git a/packages/create-mcp-app/package.json b/packages/create-mcp-app/package.json new file mode 100644 index 000000000..d7e7dc3f8 --- /dev/null +++ b/packages/create-mcp-app/package.json @@ -0,0 +1,31 @@ +{ + "name": "@modelcontextprotocol/create-mcp-app", + "version": "0.4.1", + "description": "Create MCP App projects with one command", + "type": "module", + "bin": { + "create-mcp-app": "./dist/index.js" + }, + "files": [ + "dist", + "templates" + ], + "scripts": { + "build": "tsc && chmod +x dist/index.js", + "prepublishOnly": "npm run build" + }, + "repository": { + "type": "git", + "url": "https://github.com/modelcontextprotocol/ext-apps", + "directory": "packages/create-mcp-app" + }, + "license": "MIT", + "dependencies": { + "@clack/prompts": "^0.10.0", + "picocolors": "^1.1.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.9.3" + } +} diff --git a/packages/create-mcp-app/src/cli.ts b/packages/create-mcp-app/src/cli.ts new file mode 100644 index 000000000..0cccf0131 --- /dev/null +++ b/packages/create-mcp-app/src/cli.ts @@ -0,0 +1,155 @@ +import * as p from "@clack/prompts"; +import pc from "picocolors"; +import { scaffold } from "./scaffold.js"; +import { + SDK_VERSION, + TEMPLATES, + type TemplateName, + validateProjectName, +} from "./utils.js"; + +interface CliArgs { + projectName?: string; + template?: string; + noInstall?: boolean; + help?: boolean; +} + +function parseArgs(args: string[]): CliArgs { + const result: CliArgs = {}; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === "--help" || arg === "-h") { + result.help = true; + } else if (arg === "--no-install") { + result.noInstall = true; + } else if (arg === "--template" || arg === "-t") { + result.template = args[++i]; + } else if (!arg.startsWith("-") && !result.projectName) { + result.projectName = arg; + } + } + + return result; +} + +function printHelp(): void { + console.log(` +${pc.bold("create-mcp-app")} - Scaffold MCP App projects + +${pc.bold("Usage:")} + npm create @modelcontextprotocol/mcp-app [project-name] [options] + +${pc.bold("Options:")} + -t, --template Template to use (${TEMPLATES.map((t) => t.value).join(", ")}) + --no-install Skip npm install + -h, --help Show this help message + +${pc.bold("Examples:")} + npm create @modelcontextprotocol/mcp-app + npm create @modelcontextprotocol/mcp-app my-app + npm create @modelcontextprotocol/mcp-app my-app --template react + npm create @modelcontextprotocol/mcp-app my-app --no-install +`); +} + +export async function main(): Promise { + const args = parseArgs(process.argv.slice(2)); + + if (args.help) { + printHelp(); + return; + } + + console.log(); + p.intro(pc.bgCyan(pc.black(" create-mcp-app "))); + + let projectName = args.projectName; + let template = args.template; + const runInstall = !args.noInstall; + + // Prompt for project name if not provided + if (!projectName) { + const nameResult = await p.text({ + message: "Project name:", + placeholder: "my-mcp-app", + validate: validateProjectName, + }); + + if (p.isCancel(nameResult)) { + p.cancel("Operation cancelled."); + process.exit(0); + } + + projectName = nameResult || "my-mcp-app"; + } else { + const validation = validateProjectName(projectName); + if (validation) { + p.cancel(validation); + process.exit(1); + } + } + + // Prompt for template if not provided + if (!template) { + const templateResult = await p.select({ + message: "Select a template:", + options: [...TEMPLATES], + }); + + if (p.isCancel(templateResult)) { + p.cancel("Operation cancelled."); + process.exit(0); + } + + template = templateResult as TemplateName; + } else { + const validTemplates = TEMPLATES.map((t) => t.value) as readonly string[]; + if (!validTemplates.includes(template)) { + p.cancel( + `Invalid template "${template}". Valid options: ${validTemplates.join(", ")}`, + ); + process.exit(1); + } + } + + const s = p.spinner(); + + try { + s.start("Creating project..."); + + await scaffold({ + projectName, + template: template!, + targetDir: projectName, + sdkVersion: SDK_VERSION, + }); + + s.stop("Project created!"); + + if (runInstall) { + s.start("Installing dependencies..."); + const { execSync } = await import("node:child_process"); + execSync("npm install", { + cwd: projectName, + stdio: "ignore", + }); + s.stop("Dependencies installed!"); + } + + p.note( + [ + `cd ${projectName}`, + ...(runInstall ? [] : ["npm install"]), + "npm run dev", + ].join("\n"), + "Next steps:", + ); + + p.outro(pc.green("Happy building!")); + } catch (error) { + s.stop("Failed!"); + throw error; + } +} diff --git a/packages/create-mcp-app/src/index.ts b/packages/create-mcp-app/src/index.ts new file mode 100644 index 000000000..c0be5228c --- /dev/null +++ b/packages/create-mcp-app/src/index.ts @@ -0,0 +1,7 @@ +#!/usr/bin/env node +import { main } from "./cli.js"; + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/packages/create-mcp-app/src/scaffold.ts b/packages/create-mcp-app/src/scaffold.ts new file mode 100644 index 000000000..cc076bf77 --- /dev/null +++ b/packages/create-mcp-app/src/scaffold.ts @@ -0,0 +1,88 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { + getTemplatesDir, + processTemplate, + type TemplateName, +} from "./utils.js"; + +export interface ScaffoldOptions { + projectName: string; + template: TemplateName | string; + targetDir: string; + sdkVersion: string; +} + +/** + * Copy a directory recursively, processing .tmpl files + */ +async function copyDir( + src: string, + dest: string, + replacements: Record, +): Promise { + await fs.mkdir(dest, { recursive: true }); + + const entries = await fs.readdir(src, { withFileTypes: true }); + + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + let destName = entry.name; + + // Remove .tmpl extension and process content + const isTmpl = destName.endsWith(".tmpl"); + if (isTmpl) { + destName = destName.slice(0, -5); + } + + const destPath = path.join(dest, destName); + + if (entry.isDirectory()) { + await copyDir(srcPath, destPath, replacements); + } else { + let content = await fs.readFile(srcPath, "utf-8"); + + // Always process templates for .tmpl files + if (isTmpl) { + content = processTemplate(content, replacements); + } + + await fs.writeFile(destPath, content); + } + } +} + +/** + * Scaffold a new MCP App project + */ +export async function scaffold(options: ScaffoldOptions): Promise { + const { projectName, template, targetDir, sdkVersion } = options; + const templatesDir = getTemplatesDir(); + const targetPath = path.resolve(process.cwd(), targetDir); + + // Check if target directory already exists + try { + await fs.access(targetPath); + throw new Error(`Directory "${targetDir}" already exists`); + } catch (err) { + if ((err as NodeJS.ErrnoException).code !== "ENOENT") { + throw err; + } + } + + const replacements = { + name: projectName, + sdkVersion, + }; + + // Create target directory + await fs.mkdir(targetPath, { recursive: true }); + + // Copy base template + const baseDir = path.join(templatesDir, "base"); + await copyDir(baseDir, targetPath, replacements); + + // Copy framework-specific template + const frameworkDir = path.join(templatesDir, template); + await copyDir(frameworkDir, targetPath, replacements); +} diff --git a/packages/create-mcp-app/src/utils.ts b/packages/create-mcp-app/src/utils.ts new file mode 100644 index 000000000..78deb11ff --- /dev/null +++ b/packages/create-mcp-app/src/utils.ts @@ -0,0 +1,53 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +/** Current SDK version - used in generated package.json files */ +export const SDK_VERSION = "0.4.1"; + +/** Available templates */ +export const TEMPLATES = [ + { value: "react", label: "React", hint: "React + Vite + TypeScript" }, + { + value: "vanillajs", + label: "Vanilla JS", + hint: "Vanilla JavaScript + Vite + TypeScript", + }, +] as const; + +export type TemplateName = (typeof TEMPLATES)[number]["value"]; + +/** Get the templates directory path */ +export function getTemplatesDir(): string { + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + // Works both in development (src/) and production (dist/) + return path.join(__dirname, "..", "templates"); +} + +/** Validate project name */ +export function validateProjectName(name: string): string | undefined { + if (!name) { + return undefined; // Allow empty for placeholder default + } + + if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i.test(name)) { + return "Project name must be lowercase alphanumeric with optional hyphens"; + } + + if (name.length > 214) { + return "Project name is too long (max 214 characters)"; + } + + return undefined; +} + +/** Process template placeholders in content */ +export function processTemplate( + content: string, + replacements: Record, +): string { + let result = content; + for (const [key, value] of Object.entries(replacements)) { + result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value); + } + return result; +} diff --git a/packages/create-mcp-app/templates/base/.gitignore b/packages/create-mcp-app/templates/base/.gitignore new file mode 100644 index 000000000..b94707787 --- /dev/null +++ b/packages/create-mcp-app/templates/base/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/packages/create-mcp-app/templates/base/main.ts b/packages/create-mcp-app/templates/base/main.ts new file mode 100644 index 000000000..0d0f04a69 --- /dev/null +++ b/packages/create-mcp-app/templates/base/main.ts @@ -0,0 +1,88 @@ +/** + * Entry point for running the MCP server. + * Run with: npm run serve + */ + +import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import cors from "cors"; +import type { Request, Response } from "express"; +import { createServer } from "./server.js"; + +/** + * Starts an MCP server with Streamable HTTP transport in stateless mode. + */ +export async function startStreamableHTTPServer( + createServer: () => McpServer, +): Promise { + const port = parseInt(process.env.PORT ?? "3001", 10); + + const app = createMcpExpressApp({ host: "0.0.0.0" }); + app.use(cors()); + + app.all("/mcp", async (req: Request, res: Response) => { + 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, + }); + } + } + }); + + const httpServer = app.listen(port, (err) => { + if (err) { + console.error("Failed to start server:", err); + process.exit(1); + } + console.log(`MCP server listening on http://localhost:${port}/mcp`); + }); + + const shutdown = () => { + console.log("\nShutting down..."); + httpServer.close(() => process.exit(0)); + }; + + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); +} + +/** + * Starts an MCP server with stdio transport. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + +async function main() { + if (process.argv.includes("--stdio")) { + await startStdioServer(createServer); + } else { + await startStreamableHTTPServer(createServer); + } +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/packages/create-mcp-app/templates/base/scripts/bundle-server.mjs b/packages/create-mcp-app/templates/base/scripts/bundle-server.mjs new file mode 100644 index 000000000..f7ce2188f --- /dev/null +++ b/packages/create-mcp-app/templates/base/scripts/bundle-server.mjs @@ -0,0 +1,48 @@ +/** + * Bundle server files using esbuild + */ +import * as esbuild from "esbuild"; + +// Bundle server.ts +await esbuild.build({ + entryPoints: ["server.ts"], + bundle: true, + platform: "node", + target: "node20", + format: "esm", + outdir: "dist", + external: ["@modelcontextprotocol/*", "express", "cors", "zod"], + banner: { + js: ` +import { createRequire } from 'module'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +const require = createRequire(import.meta.url); +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +`.trim(), + }, +}); + +// Bundle main.ts +await esbuild.build({ + entryPoints: ["main.ts"], + bundle: true, + platform: "node", + target: "node20", + format: "esm", + outfile: "dist/index.js", + external: ["./server.js"], + banner: { + js: `#!/usr/bin/env node +import { createRequire } from 'module'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +const require = createRequire(import.meta.url); +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +`.trim(), + }, +}); + +console.log("Server bundled successfully!"); diff --git a/packages/create-mcp-app/templates/base/server.ts b/packages/create-mcp-app/templates/base/server.ts new file mode 100644 index 000000000..fc66e0555 --- /dev/null +++ b/packages/create-mcp-app/templates/base/server.ts @@ -0,0 +1,69 @@ +import { + registerAppResource, + registerAppTool, + RESOURCE_MIME_TYPE, +} from "@modelcontextprotocol/ext-apps/server"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { + CallToolResult, + ReadResourceResult, +} from "@modelcontextprotocol/sdk/types.js"; +import fs from "node:fs/promises"; +import path from "node:path"; + +// 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; + +/** + * Creates a new MCP server instance with tools and resources registered. + */ +export function createServer(): McpServer { + const server = new McpServer({ + name: "MCP App Server", + version: "1.0.0", + }); + + // Two-part registration: tool + resource, tied together by the resource URI. + const resourceUri = "ui://get-time/mcp-app.html"; + + // Register a tool with UI metadata. When the host calls this tool, it reads + // `_meta.ui.resourceUri` to know which resource to fetch and render as an + // interactive UI. + registerAppTool( + server, + "get-time", + { + title: "Get Time", + description: "Returns the current server time as an ISO 8601 string.", + inputSchema: {}, + _meta: { ui: { resourceUri } }, // Links this tool to its UI resource + }, + async (): Promise => { + const time = new Date().toISOString(); + return { content: [{ type: "text", text: time }] }; + }, + ); + + // Register the resource, which returns the bundled HTML/JavaScript for the UI. + registerAppResource( + server, + resourceUri, + resourceUri, + { mimeType: RESOURCE_MIME_TYPE }, + async (): Promise => { + const html = await fs.readFile( + path.join(DIST_DIR, "mcp-app.html"), + "utf-8", + ); + return { + contents: [ + { uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }, + ], + }; + }, + ); + + return server; +} diff --git a/packages/create-mcp-app/templates/base/src/global.css b/packages/create-mcp-app/templates/base/src/global.css new file mode 100644 index 000000000..961537aa0 --- /dev/null +++ b/packages/create-mcp-app/templates/base/src/global.css @@ -0,0 +1,18 @@ +* { + box-sizing: border-box; +} + +html, +body { + font-family: system-ui, -apple-system, sans-serif; + font-size: 1rem; +} + +code { + font-size: 1em; +} + +#server-time { + flex: 1; + min-width: 0; +} diff --git a/packages/create-mcp-app/templates/base/tsconfig.server.json b/packages/create-mcp-app/templates/base/tsconfig.server.json new file mode 100644 index 000000000..05ddd8ec4 --- /dev/null +++ b/packages/create-mcp-app/templates/base/tsconfig.server.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "./dist", + "rootDir": ".", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true + }, + "include": ["server.ts"] +} diff --git a/packages/create-mcp-app/templates/react/mcp-app.html b/packages/create-mcp-app/templates/react/mcp-app.html new file mode 100644 index 000000000..b5a6eb95e --- /dev/null +++ b/packages/create-mcp-app/templates/react/mcp-app.html @@ -0,0 +1,14 @@ + + + + + + + MCP App + + + +
+ + + diff --git a/packages/create-mcp-app/templates/react/package.json.tmpl b/packages/create-mcp-app/templates/react/package.json.tmpl new file mode 100644 index 000000000..24349ab22 --- /dev/null +++ b/packages/create-mcp-app/templates/react/package.json.tmpl @@ -0,0 +1,37 @@ +{ + "name": "{{name}}", + "version": "0.0.1", + "type": "module", + "private": true, + "scripts": { + "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && node scripts/bundle-server.mjs", + "watch": "cross-env INPUT=mcp-app.html vite build --watch", + "serve": "tsx --watch main.ts", + "start": "cross-env NODE_ENV=development npm run build && npm run serve", + "dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve'" + }, + "dependencies": { + "@modelcontextprotocol/ext-apps": "^{{sdkVersion}}", + "@modelcontextprotocol/sdk": "^1.24.0", + "cors": "^2.8.5", + "express": "^5.1.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "zod": "^4.1.13" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.0", + "@types/node": "^22.0.0", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^4.3.4", + "concurrently": "^9.2.1", + "cross-env": "^10.1.0", + "esbuild": "^0.25.0", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "vite": "^6.0.0", + "vite-plugin-singlefile": "^2.3.0" + } +} diff --git a/packages/create-mcp-app/templates/react/src/mcp-app.module.css b/packages/create-mcp-app/templates/react/src/mcp-app.module.css new file mode 100644 index 000000000..995de04c4 --- /dev/null +++ b/packages/create-mcp-app/templates/react/src/mcp-app.module.css @@ -0,0 +1,57 @@ +.main { + --color-primary: #2563eb; + --color-primary-hover: #1d4ed8; + + width: 100%; + max-width: 425px; + box-sizing: border-box; + padding: 1rem; + + > * { + margin-top: 0; + margin-bottom: 0; + } + + > * + * { + margin-top: 1.5rem; + } +} + +.action { + > * { + margin-top: 0; + margin-bottom: 0; + width: 100%; + } + + > * + * { + margin-top: 0.5rem; + } + + > p { + display: flex; + align-items: baseline; + gap: 0.25em; + } + + textarea, + input { + font-family: inherit; + font-size: inherit; + } + + button { + padding: 0.5rem 1rem; + border: none; + border-radius: 6px; + color: white; + font-weight: bold; + background-color: var(--color-primary); + cursor: pointer; + + &:hover, + &:focus-visible { + background-color: var(--color-primary-hover); + } + } +} diff --git a/packages/create-mcp-app/templates/react/src/mcp-app.tsx b/packages/create-mcp-app/templates/react/src/mcp-app.tsx new file mode 100644 index 000000000..3d5bd8400 --- /dev/null +++ b/packages/create-mcp-app/templates/react/src/mcp-app.tsx @@ -0,0 +1,122 @@ +/** + * MCP App using React + */ +import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps"; +import { useApp } from "@modelcontextprotocol/ext-apps/react"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { StrictMode, useCallback, useEffect, useState } from "react"; +import { createRoot } from "react-dom/client"; +import styles from "./mcp-app.module.css"; + +function extractTime(callToolResult: CallToolResult): string { + const textContent = callToolResult.content?.find((c) => c.type === "text"); + return textContent && "text" in textContent ? textContent.text : "[ERROR]"; +} + +function McpApp() { + const [toolResult, setToolResult] = useState(null); + const [hostContext, setHostContext] = useState< + McpUiHostContext | undefined + >(); + + const { app, error } = useApp({ + appInfo: { name: "MCP App", version: "1.0.0" }, + capabilities: {}, + onAppCreated: (app) => { + app.onteardown = async () => { + console.info("App is being torn down"); + return {}; + }; + app.ontoolinput = async (input) => { + console.info("Received tool call input:", input); + }; + app.ontoolresult = async (result) => { + console.info("Received tool call result:", result); + setToolResult(result); + }; + app.ontoolcancelled = (params) => { + console.info("Tool call cancelled:", params.reason); + }; + app.onerror = console.error; + app.onhostcontextchanged = (params) => { + setHostContext((prev) => ({ ...prev, ...params })); + }; + }, + }); + + useEffect(() => { + if (app) { + setHostContext(app.getHostContext()); + } + }, [app]); + + if (error) + return ( +
+ ERROR: {error.message} +
+ ); + if (!app) return
Connecting...
; + + return ( + + ); +} + +interface McpAppInnerProps { + app: App; + toolResult: CallToolResult | null; + hostContext?: McpUiHostContext; +} + +function McpAppInner({ app, toolResult, hostContext }: McpAppInnerProps) { + const [serverTime, setServerTime] = useState("Loading..."); + + useEffect(() => { + if (toolResult) { + setServerTime(extractTime(toolResult)); + } + }, [toolResult]); + + const handleGetTime = useCallback(async () => { + try { + console.info("Calling get-time tool..."); + const result = await app.callServerTool({ + name: "get-time", + arguments: {}, + }); + console.info("get-time result:", result); + setServerTime(extractTime(result)); + } catch (e) { + console.error(e); + setServerTime("[ERROR]"); + } + }, [app]); + + return ( +
+

MCP App

+
+

+ Server Time:{" "} + {serverTime} +

+ +
+
+ ); +} + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/packages/create-mcp-app/templates/react/src/vite-env.d.ts b/packages/create-mcp-app/templates/react/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/packages/create-mcp-app/templates/react/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/create-mcp-app/templates/react/tsconfig.json b/packages/create-mcp-app/templates/react/tsconfig.json new file mode 100644 index 000000000..fc3c2101f --- /dev/null +++ b/packages/create-mcp-app/templates/react/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "server.ts"] +} diff --git a/packages/create-mcp-app/templates/react/vite.config.ts b/packages/create-mcp-app/templates/react/vite.config.ts new file mode 100644 index 000000000..da0af84e2 --- /dev/null +++ b/packages/create-mcp-app/templates/react/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import { viteSingleFile } from "vite-plugin-singlefile"; + +const INPUT = process.env.INPUT; +if (!INPUT) { + throw new Error("INPUT environment variable is not set"); +} + +const isDevelopment = process.env.NODE_ENV === "development"; + +export default defineConfig({ + plugins: [react(), viteSingleFile()], + build: { + sourcemap: isDevelopment ? "inline" : undefined, + cssMinify: !isDevelopment, + minify: !isDevelopment, + + rollupOptions: { + input: INPUT, + }, + outDir: "dist", + emptyOutDir: false, + }, +}); diff --git a/packages/create-mcp-app/templates/vanillajs/mcp-app.html b/packages/create-mcp-app/templates/vanillajs/mcp-app.html new file mode 100644 index 000000000..2c667f4fe --- /dev/null +++ b/packages/create-mcp-app/templates/vanillajs/mcp-app.html @@ -0,0 +1,22 @@ + + + + + + + MCP App + + +
+

MCP App

+
+

+ Server Time: + Loading... +

+ +
+
+ + + diff --git a/packages/create-mcp-app/templates/vanillajs/package.json.tmpl b/packages/create-mcp-app/templates/vanillajs/package.json.tmpl new file mode 100644 index 000000000..c04e98b70 --- /dev/null +++ b/packages/create-mcp-app/templates/vanillajs/package.json.tmpl @@ -0,0 +1,32 @@ +{ + "name": "{{name}}", + "version": "0.0.1", + "type": "module", + "private": true, + "scripts": { + "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && node scripts/bundle-server.mjs", + "watch": "cross-env INPUT=mcp-app.html vite build --watch", + "serve": "tsx --watch main.ts", + "start": "cross-env NODE_ENV=development npm run build && npm run serve", + "dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve'" + }, + "dependencies": { + "@modelcontextprotocol/ext-apps": "^{{sdkVersion}}", + "@modelcontextprotocol/sdk": "^1.24.0", + "cors": "^2.8.5", + "express": "^5.1.0", + "zod": "^4.1.13" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.0", + "@types/node": "^22.0.0", + "concurrently": "^9.2.1", + "cross-env": "^10.1.0", + "esbuild": "^0.25.0", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "vite": "^6.0.0", + "vite-plugin-singlefile": "^2.3.0" + } +} diff --git a/packages/create-mcp-app/templates/vanillajs/src/mcp-app.css b/packages/create-mcp-app/templates/vanillajs/src/mcp-app.css new file mode 100644 index 000000000..995de04c4 --- /dev/null +++ b/packages/create-mcp-app/templates/vanillajs/src/mcp-app.css @@ -0,0 +1,57 @@ +.main { + --color-primary: #2563eb; + --color-primary-hover: #1d4ed8; + + width: 100%; + max-width: 425px; + box-sizing: border-box; + padding: 1rem; + + > * { + margin-top: 0; + margin-bottom: 0; + } + + > * + * { + margin-top: 1.5rem; + } +} + +.action { + > * { + margin-top: 0; + margin-bottom: 0; + width: 100%; + } + + > * + * { + margin-top: 0.5rem; + } + + > p { + display: flex; + align-items: baseline; + gap: 0.25em; + } + + textarea, + input { + font-family: inherit; + font-size: inherit; + } + + button { + padding: 0.5rem 1rem; + border: none; + border-radius: 6px; + color: white; + font-weight: bold; + background-color: var(--color-primary); + cursor: pointer; + + &:hover, + &:focus-visible { + background-color: var(--color-primary-hover); + } + } +} diff --git a/packages/create-mcp-app/templates/vanillajs/src/mcp-app.ts b/packages/create-mcp-app/templates/vanillajs/src/mcp-app.ts new file mode 100644 index 000000000..5eb4a8a99 --- /dev/null +++ b/packages/create-mcp-app/templates/vanillajs/src/mcp-app.ts @@ -0,0 +1,89 @@ +/** + * MCP App using vanilla JavaScript + */ +import { + App, + applyDocumentTheme, + applyHostFonts, + applyHostStyleVariables, + type McpUiHostContext, +} from "@modelcontextprotocol/ext-apps"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import "./global.css"; +import "./mcp-app.css"; + +function extractTime(result: CallToolResult): string { + const textContent = result.content?.find((c) => c.type === "text"); + return textContent && "text" in textContent ? textContent.text : "[ERROR]"; +} + +const mainEl = document.querySelector(".main") as HTMLElement; +const serverTimeEl = document.getElementById("server-time")!; +const getTimeBtn = document.getElementById("get-time-btn")!; + +function handleHostContextChanged(ctx: McpUiHostContext) { + if (ctx.theme) { + applyDocumentTheme(ctx.theme); + } + if (ctx.styles?.variables) { + applyHostStyleVariables(ctx.styles.variables); + } + if (ctx.styles?.css?.fonts) { + applyHostFonts(ctx.styles.css.fonts); + } + if (ctx.safeAreaInsets) { + mainEl.style.paddingTop = `${ctx.safeAreaInsets.top}px`; + mainEl.style.paddingRight = `${ctx.safeAreaInsets.right}px`; + mainEl.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`; + mainEl.style.paddingLeft = `${ctx.safeAreaInsets.left}px`; + } +} + +// 1. Create app instance +const app = new App({ name: "MCP App", version: "1.0.0" }); + +// 2. Register handlers BEFORE connecting +app.onteardown = async () => { + console.info("App is being torn down"); + return {}; +}; + +app.ontoolinput = (params) => { + console.info("Received tool call input:", params); +}; + +app.ontoolresult = (result) => { + console.info("Received tool call result:", result); + serverTimeEl.textContent = extractTime(result); +}; + +app.ontoolcancelled = (params) => { + console.info("Tool call cancelled:", params.reason); +}; + +app.onerror = console.error; + +app.onhostcontextchanged = handleHostContextChanged; + +getTimeBtn.addEventListener("click", async () => { + try { + console.info("Calling get-time tool..."); + const result = await app.callServerTool({ + name: "get-time", + arguments: {}, + }); + console.info("get-time result:", result); + serverTimeEl.textContent = extractTime(result); + } catch (e) { + console.error(e); + serverTimeEl.textContent = "[ERROR]"; + } +}); + +// 3. Connect to host +app.connect().then(() => { + const ctx = app.getHostContext(); + if (ctx) { + handleHostContextChanged(ctx); + } +}); diff --git a/packages/create-mcp-app/templates/vanillajs/tsconfig.json b/packages/create-mcp-app/templates/vanillajs/tsconfig.json new file mode 100644 index 000000000..535267b25 --- /dev/null +++ b/packages/create-mcp-app/templates/vanillajs/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "server.ts"] +} diff --git a/packages/create-mcp-app/templates/vanillajs/vite.config.ts b/packages/create-mcp-app/templates/vanillajs/vite.config.ts new file mode 100644 index 000000000..6ff6d9979 --- /dev/null +++ b/packages/create-mcp-app/templates/vanillajs/vite.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from "vite"; +import { viteSingleFile } from "vite-plugin-singlefile"; + +const INPUT = process.env.INPUT; +if (!INPUT) { + throw new Error("INPUT environment variable is not set"); +} + +const isDevelopment = process.env.NODE_ENV === "development"; + +export default defineConfig({ + plugins: [viteSingleFile()], + build: { + sourcemap: isDevelopment ? "inline" : undefined, + cssMinify: !isDevelopment, + minify: !isDevelopment, + + rollupOptions: { + input: INPUT, + }, + outDir: "dist", + emptyOutDir: false, + }, +}); diff --git a/packages/create-mcp-app/tsconfig.json b/packages/create-mcp-app/tsconfig.json new file mode 100644 index 000000000..c83466948 --- /dev/null +++ b/packages/create-mcp-app/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true + }, + "include": ["src"] +} From 50ebcd9d98db0afec8488755d5f05aee6f32ffce Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Sun, 25 Jan 2026 22:50:47 +0200 Subject: [PATCH 2/2] docs: add create-mcp-app references to README and quickstart - Add Quick Start section to README with npm create command - Add tip callout to quickstart guide for faster project setup Co-Authored-By: Claude Opus 4.5 --- README.md | 12 ++++++++++++ docs/quickstart.md | 3 +++ 2 files changed, 15 insertions(+) diff --git a/README.md b/README.md index 84e080053..150d4723c 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,18 @@ There's no _supported_ host implementation in this repo (beyond the [examples/ba We have [contributed a tentative implementation](https://github.com/MCP-UI-Org/mcp-ui/pull/147) of hosting / iframing / sandboxing logic to the [MCP-UI](https://github.com/idosal/mcp-ui) repository, and expect OSS clients may use it, while other clients might roll their own hosting logic. +## Quick Start + +Create a new MCP App project in seconds: + +```bash +npm create @modelcontextprotocol/mcp-app my-app +cd my-app +npm run dev +``` + +Choose from React or Vanilla JS templates. See the [create-mcp-app README](packages/create-mcp-app/README.md) for all options. + ## Installation ```bash diff --git a/docs/quickstart.md b/docs/quickstart.md index 63e93713a..8d6492b9f 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -21,6 +21,9 @@ We'll use the [MCP TypeScript SDK](https://github.com/modelcontextprotocol/types You'll also need Node.js 18+. +> [!TIP] +> **Want to skip the setup?** Run `npm create @modelcontextprotocol/mcp-app my-app` to scaffold this project automatically, then skip to [Section 3: Build the View](#3-build-the-view). + ## 1. Set up the project We'll set up a minimal TypeScript project with Vite for bundling.