import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import AdmZip from "adm-zip";
import JSON5 from "json5";
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { ExpressHttpStreamableMcpServer } from "./server_runner.js";
// --------------------
// Repo settings (your public repo)
// --------------------
const OWNER = "renegademaster-droid";
const REPO = "chakra-design-system-demo";
const BRANCH = "main";
const ZIP_URL = `https://codeload.github.com/${OWNER}/${REPO}/zip/refs/heads/${BRANCH}`;
// Cache extracted files locally (inside this MCP server folder)
const CACHE_DIR = path.join(process.cwd(), ".cache", `${REPO}-${BRANCH}`);
const META_PATH = path.join(CACHE_DIR, ".meta.json");
// --------------------
// Small utilities
// --------------------
function ensureDir(p: string) {
fs.mkdirSync(p, { recursive: true });
}
function writeJson(p: string, obj: any) {
fs.writeFileSync(p, JSON.stringify(obj, null, 2), "utf-8");
}
function readJson<T>(p: string): T {
return JSON.parse(fs.readFileSync(p, "utf-8")) as T;
}
function clearDir(dir: string) {
if (!fs.existsSync(dir)) return;
for (const name of fs.readdirSync(dir)) {
fs.rmSync(path.join(dir, name), { recursive: true, force: true });
}
}
function listFilesRecursive(dir: string): string[] {
const out: string[] = [];
const items = fs.readdirSync(dir, { withFileTypes: true });
for (const it of items) {
const full = path.join(dir, it.name);
if (it.isDirectory()) out.push(...listFilesRecursive(full));
else out.push(full);
}
return out;
}
function findRepoRootAfterExtract(extractedDir: string): string {
const entries = fs.readdirSync(extractedDir, { withFileTypes: true });
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
if (dirs.length === 1) return path.join(extractedDir, dirs[0]);
for (const d of dirs) {
const candidate = path.join(extractedDir, d);
if (fs.existsSync(path.join(candidate, "src"))) return candidate;
}
return extractedDir;
}
// --------------------
// Repo download/cache (with timeout)
// --------------------
async function downloadAndExtractRepo(): Promise<string> {
ensureDir(CACHE_DIR);
const tmpZipPath = path.join(os.tmpdir(), `${REPO}-${BRANCH}-${Date.now()}.zip`);
console.error(`[ds] Downloading repo zip: ${ZIP_URL}`);
const controller = new AbortController();
const t = setTimeout(() => controller.abort(), 15000); // 15s timeout
const res = await fetch(ZIP_URL, { signal: controller.signal }).finally(() => clearTimeout(t));
if (!res.ok) throw new Error(`Download failed: ${res.status} ${res.statusText}`);
const buf = Buffer.from(await res.arrayBuffer());
fs.writeFileSync(tmpZipPath, buf);
clearDir(CACHE_DIR);
const zip = new AdmZip(tmpZipPath);
zip.extractAllTo(CACHE_DIR, true);
fs.rmSync(tmpZipPath, { force: true });
const repoRoot = findRepoRootAfterExtract(CACHE_DIR);
writeJson(META_PATH, {
owner: OWNER,
repo: REPO,
branch: BRANCH,
downloadedAt: new Date().toISOString(),
repoRoot,
});
console.error(`[ds] Extracted to: ${repoRoot}`);
return repoRoot;
}
function getCachedRepoRoot(): string | null {
if (!fs.existsSync(META_PATH)) return null;
try {
const meta = readJson<{ repoRoot: string }>(META_PATH);
if (meta?.repoRoot && fs.existsSync(meta.repoRoot)) return meta.repoRoot;
return null;
} catch {
return null;
}
}
async function ensureRepoCached(): Promise<string> {
const cached = getCachedRepoRoot();
if (cached) return cached;
return await downloadAndExtractRepo();
}
async function readRepoFile(repoRoot: string, relPath: string): Promise<string> {
const absPath = path.join(repoRoot, relPath);
const normalizedRepoRoot = path.normalize(repoRoot);
const normalizedAbs = path.normalize(absPath);
if (!normalizedAbs.startsWith(normalizedRepoRoot)) {
throw new Error("Blocked: path outside repo root");
}
if (!fs.existsSync(absPath) || fs.statSync(absPath).isDirectory()) {
throw new Error(`Not found (file): ${relPath}`);
}
return fs.readFileSync(absPath, "utf-8");
}
// --------------------
// Token parsing (JSON5)
// --------------------
// Finds first exported object literal: export const X = { ... }
function extractFirstObjectLiteral(text: string): string | null {
const idx = text.indexOf("export");
if (idx === -1) return null;
const braceStart = text.indexOf("{", idx);
if (braceStart === -1) return null;
let depth = 0;
for (let i = braceStart; i < text.length; i++) {
const ch = text[i];
if (ch === "{") depth++;
else if (ch === "}") depth--;
if (depth === 0) return text.slice(braceStart, i + 1);
}
return null;
}
function parseObjectLiteralToObject(objLiteral: string): any {
const cleaned = objLiteral.replace(/\s+as\s+const\s*/g, "");
return JSON5.parse(cleaned);
}
function flattenTokenPaths(obj: any, prefix = ""): string[] {
if (!obj || typeof obj !== "object") return [];
const out: string[] = [];
for (const [k, v] of Object.entries(obj)) {
const key = String(k);
const next = prefix ? `${prefix}/${key}` : key;
if (v && typeof v === "object" && !Array.isArray(v)) {
const vv: any = v;
const isLeaf =
Object.prototype.hasOwnProperty.call(vv, "value") ||
Object.prototype.hasOwnProperty.call(vv, "$value") ||
Object.prototype.hasOwnProperty.call(vv, "token") ||
Object.prototype.hasOwnProperty.call(vv, "type");
if (isLeaf) out.push(next);
else out.push(...flattenTokenPaths(v, next));
} else {
out.push(next);
}
}
return out;
}
async function getFigmaTokensObject(): Promise<any> {
const repoRoot = await ensureRepoCached();
const tokenFile = path.join(repoRoot, "src", "theme", "figma-tokens.ts");
if (!fs.existsSync(tokenFile)) {
throw new Error("Not found: src/theme/figma-tokens.ts");
}
const text = fs.readFileSync(tokenFile, "utf-8");
const objLit = extractFirstObjectLiteral(text);
if (!objLit) throw new Error("Could not find exported object literal in figma-tokens.ts");
return parseObjectLiteralToObject(objLit);
}
// --------------------
// Search normalization
// --------------------
function normalizeForSearch(s: string): string {
return s.trim().toLowerCase().replace(/\./g, "/").replace(/\s+/g, "/");
}
function normalizePathForSearch(p: string): string {
return p.toLowerCase().replace(/\s+/g, "/");
}
// --------------------
// Components scan
// --------------------
async function listComponentFiles(repoRoot: string): Promise<string[]> {
const srcDir = path.join(repoRoot, "src");
if (!fs.existsSync(srcDir)) return [];
const all = listFilesRecursive(srcDir)
.filter((abs) => abs.endsWith(".tsx") || abs.endsWith(".ts"))
.filter((abs) => !abs.includes(`${path.sep}theme${path.sep}`))
.filter((abs) => !abs.includes(`${path.sep}__tests__${path.sep}`))
.filter((abs) => !abs.endsWith(".d.ts"));
const rel = all.map((abs) => path.relative(repoRoot, abs).replaceAll("\\", "/"));
rel.sort((a, b) => {
const score = (p: string) =>
p.includes("/design-system/") ? 0 : p.includes("/components/") ? 1 : 2;
return score(a) - score(b) || a.localeCompare(b);
});
return rel;
}
function pickBestPathForComponent(components: string[], componentName: string): string | null {
const n = componentName.toLowerCase();
const candidates = components.filter((p) => {
const base = path.posix.basename(p).toLowerCase();
return base === `${n}.tsx` || base === `${n}.ts` || base.includes(`${n}.`);
});
if (candidates.length === 0) return null;
const score = (p: string) => {
const pl = p.toLowerCase();
if (pl.includes("/design-system/primitives/")) return 0;
if (pl.includes("/design-system/layout/")) return 1;
if (pl.includes("/design-system/foundation/")) return 2;
if (pl.includes("/components/")) return 3;
return 4;
};
candidates.sort((a, b) => score(a) - score(b) || a.localeCompare(b));
return candidates[0] ?? null;
}
function importHintFromRepoPath(rel: string): string {
return rel.replace(/^src\//, "").replace(/\.(tsx|ts)$/, "");
}
// --------------------
// Internal: build color set
// --------------------
type Intent = "login" | "dashboard" | "marketing" | "admin" | "default";
type Variant = "light" | "dark";
async function buildColorSet(intent: Intent, variant: Variant) {
const tokensObj = await getFigmaTokensObject();
const paths = Array.from(new Set(flattenTokenPaths(tokensObj)));
const norm = paths.map((p) => ({ p, n: normalizePathForSearch(p) }));
const pickExact = (...candidates: string[]) => {
for (const c of candidates) {
const cn = normalizeForSearch(c);
const found = norm.find((x) => x.n === cn);
if (found) return found.p;
}
return null;
};
const must = (label: string, value: string | null) => value ?? `MISSING:${label}`;
const bgPage = pickExact("bg/default");
const bgSurface = pickExact("bg/panel", "bg/muted", "bg/default");
const text = pickExact("text/fg", "text/fg_subtle");
const textMuted = pickExact("text/fg_muted", "text/fg_subtle", "text/fg");
const border = pickExact("border/default");
let ctaBg: string | null = null;
let ctaHoverBg: string | null = null;
let ctaText: string | null = null;
if (intent === "marketing") {
ctaBg = pickExact("primary/600", "primary/500", "secondary/600", "blue/600");
ctaHoverBg = pickExact("primary/700", "primary/600", "secondary/700", "blue/700");
ctaText = pickExact("blue/contrast", "text/fg", "gray/50");
} else if (intent === "admin") {
ctaBg = pickExact("secondary/600", "secondary/500", "primary/600", "gray/800");
ctaHoverBg = pickExact("secondary/700", "secondary/600", "primary/700", "gray/900");
ctaText = pickExact("blue/contrast", "text/fg", "gray/50");
} else {
ctaBg = pickExact("primary/600", "primary/500", "blue/600");
ctaHoverBg = pickExact("primary/700", "primary/600", "blue/700");
ctaText = pickExact("blue/contrast", "text/fg", "gray/50");
}
const focusRing = pickExact("primary/600", "blue/600");
return {
intent,
variant,
tokens: {
bgPage: must("bgPage", bgPage),
bgSurface: must("bgSurface", bgSurface),
text: must("text", text),
textMuted: must("textMuted", textMuted),
border: must("border", border),
ctaBg: must("ctaBg", ctaBg),
ctaHoverBg: must("ctaHoverBg", ctaHoverBg),
ctaText: must("ctaText", ctaText),
focusRing: must("focusRing", focusRing),
},
guidance: [
"Prefer semantic tokens (bg/*, text/*, border/*).",
"Use primary/secondary palettes for accents and CTAs.",
"Avoid hardcoded hex colors.",
],
};
}
// --------------------
// Internal: build component set
// --------------------
async function buildComponentSet(intent: Intent) {
const repoRoot = await ensureRepoCached();
const components = await listComponentFiles(repoRoot);
const common = ["Box", "Flex", "Grid", "Stack", "Text", "Button", "Card", "Input", "Dialog", "Drawer", "Tabs", "Table", "Tag", "Tooltip"];
const intentAdds: Record<Intent, string[]> = {
login: ["Input", "Button", "Card", "Text", "Stack", "Flex"],
dashboard: ["Tabs", "Table", "Card", "Tag", "Tooltip", "Drawer", "Stack", "Flex"],
marketing: ["Card", "Button", "Tabs", "Text", "Tooltip", "Stack", "Flex"],
admin: ["Table", "Tabs", "Drawer", "Tag", "Tooltip", "Stack", "Flex"],
default: ["Button", "Card", "Text", "Stack", "Flex"],
};
const wanted = Array.from(new Set([...common, ...intentAdds[intent]]));
const picks = wanted
.map((name) => {
const file = pickBestPathForComponent(components, name);
return file ? { name, file, importPathHint: importHintFromRepoPath(file) } : null;
})
.filter(Boolean) as Array<{ name: string; file: string; importPathHint: string }>;
return {
intent,
note:
"Best-effort recommendations based on file names. Prefer src/design-system/primitives/* and src/design-system/layout/*.",
recommended: picks,
nextSteps: [
"Use ds.get_component({path}) to fetch exact implementations when needed.",
"If you have barrel exports (e.g. src/design-system/index.ts), prefer importing from it.",
],
};
}
// --------------------
// Generate page TSX (small payload)
// --------------------
function generateLoginPageTsx(colorSet: any, componentSet: any) {
const have = new Set(componentSet.recommended.map((x: any) => String(x.name).toLowerCase()));
const imports: { name: string; hint: string }[] = [];
for (const want of ["Box", "Card", "Text", "Button", "Input", "Stack", "Flex"]) {
const hit = componentSet.recommended.find((x: any) => String(x.name).toLowerCase() === want.toLowerCase());
if (hit) imports.push({ name: want, hint: hit.importPathHint });
}
const importLines = imports.map((im) => `import { ${im.name} } from "${im.hint}";`).join("\n");
const BoxTag = have.has("box") ? "Box" : "div";
const CardTag = have.has("card") ? "Card" : "div";
const TextTag = have.has("text") ? "Text" : "div";
const ButtonTag = have.has("button") ? "Button" : "button";
const InputTag = have.has("input") ? "Input" : "input";
const StackTag = have.has("stack") ? "Stack" : "div";
const FlexTag = have.has("flex") ? "Flex" : "div";
const t = colorSet.tokens;
return `/* Generated login page */
${importLines}
export default function LoginPage() {
return (
<${BoxTag}
bg="${t.bgPage}"
color="${t.text}"
style={{ minHeight: "100vh", display: "grid", placeItems: "center", padding: 24 }}
>
<${CardTag}
bg="${t.bgSurface}"
borderColor="${t.border}"
style={{
width: "100%",
maxWidth: 420,
padding: 20,
borderWidth: 1,
borderStyle: "solid",
borderRadius: 16
}}
>
<${TextTag} style={{ fontSize: 20, fontWeight: 800, marginBottom: 6 }}>
Kirjaudu sisään
</${TextTag}>
<${TextTag} color="${t.textMuted}" style={{ fontSize: 13, marginBottom: 16 }}>
Tervetuloa takaisin — jatka palveluun.
</${TextTag}>
<${StackTag} style={{ display: "grid", gap: 12 }}>
<label style={{ display: "grid", gap: 6 }}>
<span style={{ fontSize: 12, opacity: 0.8 }}>Sähköposti</span>
<${InputTag} placeholder="nimi@firma.fi" style={{ width: "100%", padding: 10, borderRadius: 12 }} />
</label>
<label style={{ display: "grid", gap: 6 }}>
<span style={{ fontSize: 12, opacity: 0.8 }}>Salasana</span>
<${InputTag} type="password" placeholder="••••••••" style={{ width: "100%", padding: 10, borderRadius: 12 }} />
</label>
<${FlexTag} style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: 8 }}>
<label style={{ display: "flex", gap: 8, alignItems: "center", fontSize: 12 }}>
<input type="checkbox" />
Muista minut
</label>
<a href="#" style={{ fontSize: 12, textDecoration: "underline" }}>Unohtuiko?</a>
</${FlexTag}>
<${ButtonTag}
bg="${t.ctaBg}"
color="${t.ctaText}"
style={{ marginTop: 10, width: "100%", padding: 12, borderRadius: 12, fontWeight: 800 }}
>
Jatka
</${ButtonTag}>
</${StackTag}>
</${CardTag}>
</${BoxTag}>
);
}
`;
}
function generateDashboardPageTsx(colorSet: any, componentSet: any) {
const have = new Set(componentSet.recommended.map((x: any) => String(x.name).toLowerCase()));
const imports: { name: string; hint: string }[] = [];
for (const want of ["Box", "Card", "Text", "Button", "Stack", "Flex", "Tag"]) {
const hit = componentSet.recommended.find((x: any) => String(x.name).toLowerCase() === want.toLowerCase());
if (hit) imports.push({ name: want, hint: hit.importPathHint });
}
const importLines = imports.map((im) => `import { ${im.name} } from "${im.hint}";`).join("\n");
const BoxTag = have.has("box") ? "Box" : "div";
const CardTag = have.has("card") ? "Card" : "div";
const TextTag = have.has("text") ? "Text" : "div";
const ButtonTag = have.has("button") ? "Button" : "button";
const StackTag = have.has("stack") ? "Stack" : "div";
const FlexTag = have.has("flex") ? "Flex" : "div";
const TagTag = have.has("tag") ? "Tag" : "span";
const t = colorSet.tokens;
return `/* Generated dashboard page */
${importLines}
export default function DashboardPage() {
return (
<${BoxTag} bg="${t.bgPage}" color="${t.text}" style={{ minHeight: "100vh", padding: 24 }}>
<${FlexTag} style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
<${TextTag} style={{ fontSize: 22, fontWeight: 900 }}>Dashboard</${TextTag}>
<${ButtonTag} bg="${t.ctaBg}" color="${t.ctaText}" style={{ padding: "10px 12px", borderRadius: 12, fontWeight: 800 }}>
Uusi raportti
</${ButtonTag}>
</${FlexTag}>
<${StackTag} style={{ display: "grid", gap: 12, gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))" }}>
<${CardTag} bg="${t.bgSurface}" borderColor="${t.border}" style={{ borderWidth: 1, borderStyle: "solid", borderRadius: 16, padding: 16 }}>
<${TextTag} style={{ fontWeight: 800, marginBottom: 6 }}>Aktiiviset käyttäjät</${TextTag}>
<${TextTag} color="${t.textMuted}" style={{ fontSize: 12, marginBottom: 10 }}>Viimeiset 7 päivää</${TextTag}>
<${TextTag} style={{ fontSize: 28, fontWeight: 950 }}>12 480</${TextTag}>
</${CardTag}>
<${CardTag} bg="${t.bgSurface}" borderColor="${t.border}" style={{ borderWidth: 1, borderStyle: "solid", borderRadius: 16, padding: 16 }}>
<${TextTag} style={{ fontWeight: 800, marginBottom: 6 }}>SLA</${TextTag}>
<${TextTag} color="${t.textMuted}" style={{ fontSize: 12, marginBottom: 10 }}>Kuukauden tilanne</${TextTag}>
<${TextTag} style={{ fontSize: 28, fontWeight: 950 }}>99.9%</${TextTag}>
</${CardTag}>
<${CardTag} bg="${t.bgSurface}" borderColor="${t.border}" style={{ borderWidth: 1, borderStyle: "solid", borderRadius: 16, padding: 16 }}>
<${TextTag} style={{ fontWeight: 800, marginBottom: 6 }}>Tapahtumat</${TextTag}>
<${TextTag} color="${t.textMuted}" style={{ fontSize: 12, marginBottom: 10 }}>Tänään</${TextTag}>
<${TagTag} style={{ padding: "4px 8px", borderRadius: 999, border: "1px solid", display: "inline-block" }}>
OK
</${TagTag}>
</${CardTag}>
</${StackTag}>
</${BoxTag}>
);
}
`;
}
// --------------------
// MCP tools
// --------------------
function registerTools(server: McpServer) {
server.tool(
"ds.search_token_paths",
{ query: z.string().min(1) },
async ({ query }) => {
try {
const q = normalizeForSearch(query);
const tokensObj = await getFigmaTokensObject();
const paths = Array.from(new Set(flattenTokenPaths(tokensObj)));
const norm = paths.map((p) => ({ p, n: normalizePathForSearch(p) }));
if (q === "colors" || q === "color") {
const buckets = ["bg/", "text/", "border/", "primary/", "secondary/", "success/", "warning/", "error/", "info/", "blue/", "gray/"];
const hits = norm
.filter((x) => buckets.some((b) => x.n.startsWith(b)))
.map((x) => x.p)
.sort()
.slice(0, 200);
return { content: [{ type: "text", text: JSON.stringify(hits, null, 2) }] };
}
const hits = norm
.filter((x) => x.n.includes(q))
.map((x) => x.p)
.sort()
.slice(0, 200);
return { content: [{ type: "text", text: JSON.stringify(hits, null, 2) }] };
} catch (e: any) {
return { content: [{ type: "text", text: `ERROR in ds.search_token_paths: ${e?.message ?? String(e)}` }] };
}
}
);
server.tool("ds.get_tokens", async () => {
try {
const tokensObj = await getFigmaTokensObject();
const paths = Array.from(new Set(flattenTokenPaths(tokensObj))).sort();
const colorBuckets = ["bg/", "text/", "border/", "primary/", "secondary/", "success/", "warning/", "error/", "info/", "blue/", "gray/"];
const sampleColorRelated = paths.filter((p) => colorBuckets.some((b) => normalizePathForSearch(p).startsWith(b))).slice(0, 120);
return {
content: [
{
type: "text",
text: JSON.stringify(
{
totalPaths: paths.length,
sampleFirstPaths: paths.slice(0, 60),
sampleColorRelated,
},
null,
2
),
},
],
};
} catch (e: any) {
return { content: [{ type: "text", text: `ERROR in ds.get_tokens: ${e?.message ?? String(e)}` }] };
}
});
server.tool("ds.get_components", async () => {
const repoRoot = await ensureRepoCached();
const components = await listComponentFiles(repoRoot);
return {
content: [
{
type: "text",
text: JSON.stringify(
{ count: components.length, components },
null,
2
),
},
],
};
});
server.tool(
"ds.get_component",
{ path: z.string().min(1) },
async ({ path: relPath }) => {
const repoRoot = await ensureRepoCached();
try {
const text = await readRepoFile(repoRoot, relPath);
return { content: [{ type: "text", text }] };
} catch (e: any) {
return { content: [{ type: "text", text: `Error: ${e?.message ?? String(e)}` }] };
}
}
);
server.tool(
"ds.pick_color_set",
{ intent: z.enum(["login", "dashboard", "marketing", "admin", "default"]).optional(), variant: z.enum(["light", "dark"]).optional() },
async ({ intent = "default", variant = "light" }) => {
try {
const result = await buildColorSet(intent, variant);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
} catch (e: any) {
return { content: [{ type: "text", text: `ERROR in ds.pick_color_set: ${e?.message ?? String(e)}` }] };
}
}
);
server.tool(
"ds.pick_component_set",
{ intent: z.enum(["login", "dashboard", "marketing", "admin", "default"]).optional() },
async ({ intent = "default" }) => {
try {
const result = await buildComponentSet(intent);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
} catch (e: any) {
return { content: [{ type: "text", text: `ERROR in ds.pick_component_set: ${e?.message ?? String(e)}` }] };
}
}
);
// ✅ generate_page: default returns TSX only (small payload)
server.tool(
"ds.generate_page",
{
intent: z.enum(["login", "dashboard", "marketing", "admin", "default"]).optional(),
variant: z.enum(["light", "dark"]).optional(),
format: z.enum(["tsx", "json"]).optional(),
},
async ({ intent = "default", variant = "light", format = "tsx" }) => {
try {
console.error("[ds.generate_page] start", { intent, variant, format });
const colorSet = await buildColorSet(intent, variant);
const componentSet = await buildComponentSet(intent);
let filePath = "";
let tsx = "";
if (intent === "dashboard") {
filePath = "src/pages/DashboardPage.tsx";
tsx = generateDashboardPageTsx(colorSet, componentSet);
} else {
// default to login
filePath = "src/pages/LoginPage.tsx";
tsx = generateLoginPageTsx(colorSet, componentSet);
}
console.error("[ds.generate_page] done", { filePath, length: tsx.length });
if (format === "tsx") {
const header =
`/* ${filePath} (generated) */\n` +
`/* tokens: bg=${colorSet.tokens.bgPage}, surface=${colorSet.tokens.bgSurface}, text=${colorSet.tokens.text}, cta=${colorSet.tokens.ctaBg} */\n\n`;
return { content: [{ type: "text", text: header + tsx }] };
}
const result = {
intent,
variant,
filePath,
tokensUsed: colorSet.tokens,
componentsRecommended: componentSet.recommended,
content: tsx,
};
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
} catch (e: any) {
return { content: [{ type: "text", text: `ERROR in ds.generate_page: ${e?.message ?? String(e)}` }] };
}
}
);
}
// --------------------
// Start Streamable HTTP server (template runner)
// --------------------
const PORT = Number(process.env.PORT ?? 3000);
const options: any = {
name: "chakra-design-system-demo-mcp",
port: PORT, // if your runner uses listenPort, change this
};
ExpressHttpStreamableMcpServer(options, registerTools);