@@ -3,8 +3,16 @@ import { join } from 'path';
33import prompts from 'prompts' ;
44import { validateMCPProject } from '../utils/validate-project.js' ;
55import { toPascalCase } from '../utils/string-utils.js' ;
6-
7- export async function addApp ( name ?: string ) {
6+ import {
7+ generateReactHtmlShell ,
8+ generateReactApp ,
9+ generateReactStyles ,
10+ generateViteConfig ,
11+ generateTsconfigApp ,
12+ getReactInstallInstructions ,
13+ } from '../templates/react-app.js' ;
14+
15+ export async function addApp ( name ?: string , options ?: { react ?: boolean } ) {
816 await validateMCPProject ( ) ;
917
1018 let appName = name ;
@@ -33,6 +41,7 @@ export async function addApp(name?: string) {
3341 throw new Error ( 'App name is required' ) ;
3442 }
3543
44+ const useReact = options ?. react ?? false ;
3645 const className = toPascalCase ( appName ) ;
3746 const fileName = `${ className } App.ts` ;
3847 const appsDir = join ( process . cwd ( ) , 'src/apps' ) ;
@@ -42,22 +51,83 @@ export async function addApp(name?: string) {
4251 await mkdir ( appsDir , { recursive : true } ) ;
4352 await mkdir ( viewsDir , { recursive : true } ) ;
4453
45- const appContent = generateAppClass ( appName , className ) ;
46- const htmlContent = generateHtmlView ( appName , className ) ;
47-
48- await writeFile ( join ( appsDir , fileName ) , appContent ) ;
49- await writeFile ( join ( viewsDir , 'index.html' ) , htmlContent ) ;
50-
51- console . log ( `App ${ appName } created successfully:` ) ;
52- console . log ( ` - App class: src/apps/${ fileName } ` ) ;
53- console . log ( ` - HTML view: src/app-views/${ appName } /index.html` ) ;
54+ if ( useReact ) {
55+ await writeFile ( join ( appsDir , fileName ) , generateReactAppClass ( appName , className ) ) ;
56+ await writeFile ( join ( viewsDir , 'index.html' ) , generateReactHtmlShell ( appName ) ) ;
57+ await writeFile ( join ( viewsDir , 'App.tsx' ) , generateReactApp ( appName , className ) ) ;
58+ await writeFile ( join ( viewsDir , 'styles.css' ) , generateReactStyles ( ) ) ;
59+ await writeFile ( join ( viewsDir , 'vite.config.ts' ) , generateViteConfig ( ) ) ;
60+ await writeFile ( join ( viewsDir , 'tsconfig.json' ) , generateTsconfigApp ( ) ) ;
61+
62+ console . log ( `React app ${ appName } created successfully:` ) ;
63+ console . log ( ` - App class: src/apps/${ fileName } ` ) ;
64+ console . log ( ` - React entry: src/app-views/${ appName } /App.tsx` ) ;
65+ console . log ( ` - Styles: src/app-views/${ appName } /styles.css` ) ;
66+ console . log ( ` - Vite config: src/app-views/${ appName } /vite.config.ts` ) ;
67+ console . log ( getReactInstallInstructions ( ) . replace ( / < n a m e > / g, appName ) ) ;
68+ } else {
69+ await writeFile ( join ( appsDir , fileName ) , generateVanillaAppClass ( appName , className ) ) ;
70+ await writeFile ( join ( viewsDir , 'index.html' ) , generateVanillaHtmlView ( appName , className ) ) ;
71+
72+ console . log ( `App ${ appName } created successfully:` ) ;
73+ console . log ( ` - App class: src/apps/${ fileName } ` ) ;
74+ console . log ( ` - HTML view: src/app-views/${ appName } /index.html` ) ;
75+ }
5476 } catch ( error ) {
5577 console . error ( 'Error creating app:' , error ) ;
5678 process . exit ( 1 ) ;
5779 }
5880}
5981
60- function generateAppClass ( appName : string , className : string ) : string {
82+ // ── React App Class (Mode A) ──────────────────────────────────────────────────
83+
84+ function generateReactAppClass ( appName : string , className : string ) : string {
85+ return `import { MCPApp } from "mcp-framework";
86+ import { z } from "zod";
87+ import { readFileSync } from "fs";
88+ import { join, dirname } from "path";
89+ import { fileURLToPath } from "url";
90+
91+ const __dirname = dirname(fileURLToPath(import.meta.url));
92+
93+ class ${ className } App extends MCPApp {
94+ name = "${ appName } ";
95+
96+ ui = {
97+ resourceUri: "ui://${ appName } /view",
98+ resourceName: "${ className } ",
99+ resourceDescription: "${ className } interactive view",
100+ };
101+
102+ getContent() {
103+ // Reads the Vite-bundled single HTML file
104+ return readFileSync(
105+ join(__dirname, "../../app-views/${ appName } /dist/index.html"),
106+ "utf-8"
107+ );
108+ }
109+
110+ tools = [
111+ {
112+ name: "${ appName } _show",
113+ description: "Display the ${ className } view",
114+ schema: z.object({
115+ query: z.string().describe("Input query"),
116+ }),
117+ execute: async (input: { query: string }) => {
118+ return { result: \`Processed: \${input.query}\` };
119+ },
120+ },
121+ ];
122+ }
123+
124+ export default ${ className } App;
125+ ` ;
126+ }
127+
128+ // ── Vanilla Templates (unchanged) ─────────────────────────────────────────────
129+
130+ function generateVanillaAppClass ( appName : string , className : string ) : string {
61131 return `import { MCPApp } from "mcp-framework";
62132import { z } from "zod";
63133import { readFileSync } from "fs";
@@ -100,7 +170,7 @@ export default ${className}App;
100170` ;
101171}
102172
103- function generateHtmlView ( appName : string , className : string ) : string {
173+ function generateVanillaHtmlView ( appName : string , className : string ) : string {
104174 return `<!DOCTYPE html>
105175<html lang="en">
106176<head>
@@ -151,23 +221,20 @@ function generateHtmlView(appName: string, className: string): string {
151221 protocolVersion: "2026-01-26",
152222 });
153223
154- // Apply host theme
155224 const vars = init.hostContext?.styles?.variables;
156225 if (vars) {
157226 for (const [key, value] of Object.entries(vars)) {
158227 if (value) document.documentElement.style.setProperty(key, value);
159228 }
160229 }
161230
162- // Handle tool input
163231 onNotification("ui/notifications/tool-input", (params) => {
164232 document.getElementById("app").innerHTML =
165233 "<h2>${ className } </h2><pre>" +
166234 JSON.stringify(params.arguments, null, 2) +
167235 "</pre>";
168236 });
169237
170- // Handle tool result
171238 onNotification("ui/notifications/tool-result", (params) => {
172239 const text = params.content?.[0]?.text ?? JSON.stringify(params);
173240 document.getElementById("app").innerHTML =
0 commit comments