Skip to content
This repository was archived by the owner on May 21, 2026. It is now read-only.

Commit fe861bf

Browse files
committed
Merge branch 'feat/mcp-ui-apps' of https://github.com/mcp-use/mcp-use-ts into feat/mcp-ui-apps
2 parents 9f43c93 + f013d0d commit fe861bf

3 files changed

Lines changed: 134 additions & 184 deletions

File tree

examples/kanban-app/scripts/build.mts

Lines changed: 0 additions & 123 deletions
This file was deleted.

packages/cli/src/build.ts

Lines changed: 132 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { promises as fs } from 'node:fs'
22
import path from 'node:path'
3-
import { build } from 'esbuild'
3+
import { build, context } from 'esbuild'
44
import { globby } from 'globby'
55

66
const ROUTE_PREFIX = '/mcp-use/widgets'
@@ -43,8 +43,62 @@ function htmlTemplate({ title, scriptPath }: { title: string, scriptPath: string
4343
</html>`
4444
}
4545

46-
export async function buildWidgets(projectPath: string) {
47-
console.log('🔨 Building UI widgets with esbuild...')
46+
async function buildWidget(entry: string, projectPath: string, minify = true) {
47+
const relativePath = path.relative(projectPath, entry)
48+
const route = toRoute(relativePath)
49+
const pageOutDir = path.join(projectPath, outDirForRoute(route))
50+
const baseName = path.parse(entry).name
51+
52+
// Build JS/CSS chunks for this page
53+
await build({
54+
entryPoints: [entry],
55+
bundle: true,
56+
splitting: true,
57+
format: 'esm',
58+
platform: 'browser',
59+
target: 'es2018',
60+
sourcemap: !minify,
61+
minify,
62+
outdir: path.join(pageOutDir, 'assets'),
63+
logLevel: 'silent',
64+
loader: {
65+
'.svg': 'file',
66+
'.png': 'file',
67+
'.jpg': 'file',
68+
'.jpeg': 'file',
69+
'.gif': 'file',
70+
'.css': 'css',
71+
},
72+
entryNames: `[name]-[hash]`,
73+
chunkNames: `chunk-[hash]`,
74+
assetNames: `asset-[hash]`,
75+
define: {
76+
'process.env.NODE_ENV': minify ? '"production"' : '"development"',
77+
},
78+
})
79+
80+
// Find the main entry file name
81+
const files = await fs.readdir(path.join(pageOutDir, 'assets'))
82+
const mainJs = files.find(f => f.startsWith(`${baseName}-`) && f.endsWith('.js'))
83+
if (!mainJs)
84+
throw new Error(`Failed to locate entry JS for ${entry}`)
85+
86+
// Write an index.html that points to the entry
87+
await fs.mkdir(pageOutDir, { recursive: true })
88+
await fs.writeFile(
89+
path.join(pageOutDir, 'index.html'),
90+
htmlTemplate({
91+
title: baseName,
92+
scriptPath: `./assets/${mainJs}`,
93+
}),
94+
'utf8',
95+
)
96+
97+
return { baseName, route }
98+
}
99+
100+
export async function buildWidgets(projectPath: string, watch = false) {
101+
console.log(`🔨 Building UI widgets with esbuild${watch ? ' (watch mode)' : ''}...`)
48102

49103
const srcDir = path.join(projectPath, SRC_DIR)
50104
const outDir = path.join(projectPath, OUT_DIR)
@@ -56,63 +110,81 @@ export async function buildWidgets(projectPath: string) {
56110
const entries = await globby([`${srcDir}/**/*.tsx`])
57111
console.log(`📦 Found ${entries.length} widget files`)
58112

59-
// Build each entry as an isolated page with hashed output
60-
for (const entry of entries) {
61-
const relativePath = path.relative(projectPath, entry)
62-
const route = toRoute(relativePath)
63-
const pageOutDir = path.join(projectPath, outDirForRoute(route))
64-
const baseName = path.parse(entry).name
65-
66-
console.log(`🔨 Building ${baseName}...`)
67-
68-
// Build JS/CSS chunks for this page
69-
await build({
70-
entryPoints: [entry],
71-
bundle: true,
72-
splitting: true,
73-
format: 'esm',
74-
platform: 'browser',
75-
target: 'es2018',
76-
sourcemap: false,
77-
minify: true,
78-
outdir: path.join(pageOutDir, 'assets'),
79-
logLevel: 'silent',
80-
loader: {
81-
'.svg': 'file',
82-
'.png': 'file',
83-
'.jpg': 'file',
84-
'.jpeg': 'file',
85-
'.gif': 'file',
86-
'.css': 'css',
87-
},
88-
entryNames: `[name]-[hash]`,
89-
chunkNames: `chunk-[hash]`,
90-
assetNames: `asset-[hash]`,
91-
define: {
92-
'process.env.NODE_ENV': '"production"',
93-
},
94-
})
95-
96-
// Find the main entry file name
97-
const files = await fs.readdir(path.join(pageOutDir, 'assets'))
98-
const mainJs = files.find(f => f.startsWith(`${baseName}-`) && f.endsWith('.js'))
99-
if (!mainJs)
100-
throw new Error(`Failed to locate entry JS for ${entry}`)
101-
102-
// Write an index.html that points to the entry
103-
await fs.mkdir(pageOutDir, { recursive: true })
104-
await fs.writeFile(
105-
path.join(pageOutDir, 'index.html'),
106-
htmlTemplate({
107-
title: baseName,
108-
scriptPath: `./assets/${mainJs}`,
109-
}),
110-
'utf8',
111-
)
112-
113-
console.log(`✅ Built ${baseName} -> ${route}`)
113+
if (watch) {
114+
// Watch mode
115+
for (const entry of entries) {
116+
const relativePath = path.relative(projectPath, entry)
117+
const route = toRoute(relativePath)
118+
const pageOutDir = path.join(projectPath, outDirForRoute(route))
119+
const baseName = path.parse(entry).name
120+
121+
const ctx = await context({
122+
entryPoints: [entry],
123+
bundle: true,
124+
splitting: true,
125+
format: 'esm',
126+
platform: 'browser',
127+
target: 'es2018',
128+
sourcemap: true,
129+
minify: false,
130+
outdir: path.join(pageOutDir, 'assets'),
131+
logLevel: 'info',
132+
loader: {
133+
'.svg': 'file',
134+
'.png': 'file',
135+
'.jpg': 'file',
136+
'.jpeg': 'file',
137+
'.gif': 'file',
138+
'.css': 'css',
139+
},
140+
entryNames: `[name]-[hash]`,
141+
chunkNames: `chunk-[hash]`,
142+
assetNames: `asset-[hash]`,
143+
define: {
144+
'process.env.NODE_ENV': '"development"',
145+
},
146+
plugins: [{
147+
name: 'html-writer',
148+
setup(build) {
149+
build.onEnd(async () => {
150+
try {
151+
const files = await fs.readdir(path.join(pageOutDir, 'assets'))
152+
const mainJs = files.find(f => f.startsWith(`${baseName}-`) && f.endsWith('.js'))
153+
if (mainJs) {
154+
await fs.mkdir(pageOutDir, { recursive: true })
155+
await fs.writeFile(
156+
path.join(pageOutDir, 'index.html'),
157+
htmlTemplate({
158+
title: baseName,
159+
scriptPath: `./assets/${mainJs}`,
160+
}),
161+
'utf8',
162+
)
163+
}
164+
} catch (err) {
165+
console.error(`Error writing HTML for ${baseName}:`, err)
166+
}
167+
})
168+
},
169+
}],
170+
})
171+
172+
await ctx.watch()
173+
console.log(`👀 Watching ${baseName}...`)
174+
}
175+
176+
console.log('👀 Watching for changes... Press Ctrl+C to stop.')
177+
}
178+
else {
179+
// Build once
180+
for (const entry of entries) {
181+
console.log(`🔨 Building ${path.parse(entry).name}...`)
182+
const { baseName, route } = await buildWidget(entry, projectPath)
183+
console.log(`✅ Built ${baseName} -> ${route}`)
184+
}
185+
186+
console.log('🎉 Build complete!')
114187
}
115-
116-
console.log('🎉 Build complete!')
117188
}
118189

190+

packages/cli/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ program
1313
.command('build')
1414
.description('Build MCP UI widgets')
1515
.option('-p, --path <path>', 'Path to project directory', process.cwd())
16+
.option('-w, --watch', 'Watch for changes and rebuild')
1617
.action(async (options) => {
1718
try {
18-
await buildWidgets(options.path);
19+
await buildWidgets(options.path, options.watch);
1920
} catch (error) {
2021
console.error('Build failed:', error);
2122
process.exit(1);

0 commit comments

Comments
 (0)