sonance-brand-mcp 1.3.116 → 1.3.118

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import * as os from "os";
8
8
  import { fileURLToPath } from "url";
9
9
  import { execSync } from "child_process";
10
10
  import sharp from "sharp";
11
+ import { formatCodeBlock, } from "./utils/response-formatter.js";
11
12
  // ============================================
12
13
  // INSTALLER (--init flag)
13
14
  // ============================================
@@ -2634,6 +2635,25 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2634
2635
  required: ["logo_path"],
2635
2636
  },
2636
2637
  },
2638
+ {
2639
+ name: "get_logo_base64",
2640
+ description: "Returns a logo as base64-encoded data URL ready for embedding in React artifacts, HTML, or web applications. USE THIS for artifacts instead of drawing/approximating logos. Returns a data URL that works directly in <img src='...'> tags. CRITICAL: NEVER draw, recreate, or approximate logos with SVG, CSS, or styled text - always use this tool to get the actual image data.",
2641
+ inputSchema: {
2642
+ type: "object",
2643
+ properties: {
2644
+ logo_path: {
2645
+ type: "string",
2646
+ description: "Path to the logo from list_logos output (e.g., 'sonance/Sonance_Logo_2C_Dark_RGB.png')",
2647
+ },
2648
+ format: {
2649
+ type: "string",
2650
+ enum: ["dataUrl", "base64", "both"],
2651
+ description: "Output format. 'dataUrl' for img src (default), 'base64' for raw data, 'both' for both formats",
2652
+ },
2653
+ },
2654
+ required: ["logo_path"],
2655
+ },
2656
+ },
2637
2657
  {
2638
2658
  name: "diagnose_logos",
2639
2659
  description: "Diagnose logo loading issues. Returns validation status, path resolution examples, and environment info. Use this when logos fail to embed or appear in responses.",
@@ -2915,7 +2935,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2915
2935
  try {
2916
2936
  const content = fs.readFileSync(getAssetPath("css"), "utf-8");
2917
2937
  return {
2918
- content: [{ type: "text", text: `/* Sonance Brand Theme - globals.css */\n\n${content}` }],
2938
+ content: [{
2939
+ type: "text",
2940
+ text: `# Sonance Brand Theme
2941
+
2942
+ ${formatCodeBlock(content, { language: "css", filename: "globals.css" })}`
2943
+ }],
2919
2944
  };
2920
2945
  }
2921
2946
  catch (e) {
@@ -2964,10 +2989,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2964
2989
  const match = files.find(f => f.toLowerCase() === `${componentName}.tsx`);
2965
2990
  if (match) {
2966
2991
  const content = fs.readFileSync(path.join(componentsDir, match), "utf-8");
2992
+ const matchedName = match.replace('.tsx', '');
2967
2993
  return {
2968
2994
  content: [{
2969
2995
  type: "text",
2970
- text: `// File: components/ui/${match}\n\n${content}`
2996
+ text: `# ${matchedName} Component
2997
+
2998
+ ${formatCodeBlock(content, { language: "tsx", filename: `components/ui/${match}` })}`
2971
2999
  }],
2972
3000
  };
2973
3001
  }
@@ -2980,7 +3008,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2980
3008
  return {
2981
3009
  content: [{
2982
3010
  type: "text",
2983
- text: `// File: components/ui/${componentName}.tsx\n\n${content}`
3011
+ text: `# ${componentName} Component
3012
+
3013
+ ${formatCodeBlock(content, { language: "tsx", filename: `components/ui/${componentName}.tsx` })}`
2984
3014
  }],
2985
3015
  };
2986
3016
  }
@@ -2997,7 +3027,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2997
3027
  return {
2998
3028
  content: [{
2999
3029
  type: "text",
3000
- text: `// File: lib/utils.ts\n// These utilities are used by Sonance UI components\n\n${content}`
3030
+ text: `# Sonance UI Utilities
3031
+
3032
+ These utilities are used by Sonance UI components.
3033
+
3034
+ ${formatCodeBlock(content, { language: "typescript", filename: "lib/utils.ts" })}`
3001
3035
  }],
3002
3036
  };
3003
3037
  }
@@ -3094,6 +3128,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3094
3128
  const base64Data = imageBuffer.toString('base64');
3095
3129
  const mimeType = getImageMimeType(resolvedPath);
3096
3130
  const fileName = path.basename(resolvedPath);
3131
+ // Create data URL for artifact use
3132
+ const dataUrl = `data:${mimeType};base64,${base64Data}`;
3097
3133
  // Determine logo variant info for recommendations
3098
3134
  const isLightLogo = /light|reverse|white/i.test(fileName);
3099
3135
  const isDarkLogo = /dark|black/i.test(fileName);
@@ -3124,28 +3160,45 @@ ${variantSuggestion}
3124
3160
 
3125
3161
  ---
3126
3162
 
3127
- ### Implementation (Next.js with next/image)
3163
+ ## CRITICAL: Logo Usage Rules
3164
+
3165
+ > **NEVER draw, recreate, or approximate this logo.** This includes:
3166
+ > - SVG drawings or paths
3167
+ > - Styled text (e.g., colored letters spelling "SONANCE")
3168
+ > - CSS recreations or shapes
3169
+ > - Unicode character approximations
3170
+ >
3171
+ > **ALWAYS use the actual image data provided below.**
3172
+
3173
+ ---
3174
+
3175
+ ### For React Artifacts (USE THIS - includes actual image data)
3176
+ \`\`\`tsx
3177
+ <img
3178
+ src="${dataUrl}"
3179
+ alt="${brandName}"
3180
+ className="h-10 w-auto"
3181
+ />
3182
+ \`\`\`
3183
+
3184
+ ### For Next.js Projects (with file system access)
3128
3185
  \`\`\`tsx
3129
3186
  import Image from 'next/image';
3130
3187
 
3131
- // In your component:
3132
3188
  <Image
3133
3189
  src="${normalizedPath}"
3134
3190
  alt="${brandName}"
3135
3191
  width={150}
3136
3192
  height={40}
3137
3193
  className="h-10 w-auto"
3138
- priority // Add if logo is above the fold
3194
+ priority
3139
3195
  />
3140
3196
  \`\`\`
3141
3197
 
3142
- ### Implementation (React/HTML)
3143
- \`\`\`tsx
3144
- <img
3145
- src="${normalizedPath}"
3146
- alt="${brandName}"
3147
- className="h-10 w-auto"
3148
- />
3198
+ ### Base64 Data URL (copy for artifacts)
3199
+ Use this data URL directly in img src for artifacts:
3200
+ \`\`\`
3201
+ ${dataUrl}
3149
3202
  \`\`\`
3150
3203
 
3151
3204
  ### With Dark Mode Support
@@ -3154,12 +3207,12 @@ import Image from 'next/image';
3154
3207
  <img
3155
3208
  src="${normalizedPath}"
3156
3209
  alt="${brandName}"
3157
- className="h-10 w-auto dark:hidden" {/* Light mode */}
3210
+ className="h-10 w-auto dark:hidden"
3158
3211
  />
3159
3212
  <img
3160
3213
  src="${normalizedPath.replace(/dark/gi, 'Light').replace(/black/gi, 'Reverse')}"
3161
3214
  alt="${brandName}"
3162
- className="h-10 w-auto hidden dark:block" {/* Dark mode */}
3215
+ className="h-10 w-auto hidden dark:block"
3163
3216
  />
3164
3217
  \`\`\`
3165
3218
 
@@ -3196,6 +3249,98 @@ import Image from 'next/image';
3196
3249
  };
3197
3250
  }
3198
3251
  }
3252
+ case "get_logo_base64": {
3253
+ const logoArgs = args;
3254
+ const logoPath = logoArgs.logo_path;
3255
+ const format = logoArgs.format || "dataUrl";
3256
+ if (!logoPath) {
3257
+ return {
3258
+ content: [{ type: "text", text: "Error: logo_path is required. Use list_logos to see available logos." }],
3259
+ isError: true,
3260
+ };
3261
+ }
3262
+ try {
3263
+ const resolvedPath = resolveLogoPath(logoPath);
3264
+ if (!resolvedPath) {
3265
+ return {
3266
+ content: [{ type: "text", text: `Error: Invalid logo path: ${logoPath}` }],
3267
+ isError: true,
3268
+ };
3269
+ }
3270
+ if (!fs.existsSync(resolvedPath)) {
3271
+ return {
3272
+ content: [{ type: "text", text: `Error: Logo not found: ${logoPath}. Use list_logos to see available logos.` }],
3273
+ isError: true,
3274
+ };
3275
+ }
3276
+ // Read and encode the image
3277
+ const imageBuffer = fs.readFileSync(resolvedPath);
3278
+ const base64Data = imageBuffer.toString('base64');
3279
+ const mimeType = getImageMimeType(resolvedPath);
3280
+ const fileName = path.basename(resolvedPath);
3281
+ const dataUrl = `data:${mimeType};base64,${base64Data}`;
3282
+ // Determine brand for alt text
3283
+ const brandMatch = logoPath.match(/\/(sonance|iport|blaze|james|trufig)/i);
3284
+ const brandName = brandMatch ? brandMatch[1].charAt(0).toUpperCase() + brandMatch[1].slice(1) : "Brand";
3285
+ // Build response based on format
3286
+ let responseText = `## Logo for Artifacts: ${fileName}
3287
+
3288
+ > **CRITICAL:** Use ONLY this image data. NEVER draw, recreate, or approximate this logo with SVG, CSS, styled text, or any other method.
3289
+
3290
+ ### Ready-to-Use Code (copy this directly)
3291
+ \`\`\`tsx
3292
+ <img
3293
+ src="${dataUrl}"
3294
+ alt="${brandName}"
3295
+ className="h-10 w-auto"
3296
+ />
3297
+ \`\`\`
3298
+ `;
3299
+ if (format === "dataUrl" || format === "both") {
3300
+ responseText += `
3301
+ ### Data URL (use in img src)
3302
+ \`\`\`
3303
+ ${dataUrl}
3304
+ \`\`\`
3305
+ `;
3306
+ }
3307
+ if (format === "base64" || format === "both") {
3308
+ responseText += `
3309
+ ### Raw Base64 Data
3310
+ \`\`\`
3311
+ ${base64Data}
3312
+ \`\`\`
3313
+
3314
+ **MIME Type:** ${mimeType}
3315
+ `;
3316
+ }
3317
+ responseText += `
3318
+ ---
3319
+
3320
+ **File:** ${fileName}
3321
+ **Size:** ${imageBuffer.length} bytes
3322
+ `;
3323
+ return {
3324
+ content: [
3325
+ {
3326
+ type: "image",
3327
+ data: base64Data,
3328
+ mimeType: mimeType,
3329
+ },
3330
+ {
3331
+ type: "text",
3332
+ text: responseText,
3333
+ },
3334
+ ],
3335
+ };
3336
+ }
3337
+ catch (e) {
3338
+ return {
3339
+ content: [{ type: "text", text: `Error reading logo: ${e}` }],
3340
+ isError: true,
3341
+ };
3342
+ }
3343
+ }
3199
3344
  case "diagnose_logos": {
3200
3345
  // Run validation and collect diagnostic info
3201
3346
  const validation = validateLogoMap();
@@ -3280,36 +3425,73 @@ ${!validation.valid ? `
3280
3425
  }
3281
3426
  case "get_full_library": {
3282
3427
  try {
3283
- let fullLibrary = "# Sonance Component Library\n\n";
3284
- fullLibrary += "This is the complete Sonance UI component library. Copy these files into your project.\n\n";
3285
- // Add brand guidelines
3286
- fullLibrary += "---\n\n## 1. Brand Guidelines\n\n";
3428
+ // Build structured response with multiple content blocks for artifact rendering
3429
+ const contentBlocks = [];
3430
+ // Validate and embed logos first
3431
+ validateLogoMap();
3432
+ const lightLogoPath = LOGO_MAP.default.light;
3433
+ const darkLogoPath = LOGO_MAP.default.dark;
3434
+ await embedLogoWithFeedback(lightLogoPath, "Brand Logo (for Light Backgrounds)", contentBlocks);
3435
+ await embedLogoWithFeedback(darkLogoPath, "Brand Logo (for Dark Backgrounds)", contentBlocks);
3436
+ // Section 1: Overview
3437
+ contentBlocks.push({
3438
+ type: "text",
3439
+ text: `# Sonance Component Library
3440
+
3441
+ This is the complete Sonance UI component library. Copy these files into your project.
3442
+
3443
+ ## Table of Contents
3444
+ 1. [Brand Guidelines](#brand-guidelines)
3445
+ 2. [CSS Theme](#css-theme)
3446
+ 3. [Utilities](#utilities)
3447
+ 4. [UI Components](#ui-components)
3448
+ 5. [Brand Logos](#brand-logos)`
3449
+ });
3450
+ // Section 2: Brand Guidelines
3451
+ let guidelines = "*Brand guidelines not found*";
3287
3452
  try {
3288
- fullLibrary += fs.readFileSync(getAssetPath("guidelines"), "utf-8");
3453
+ guidelines = fs.readFileSync(getAssetPath("guidelines"), "utf-8");
3289
3454
  }
3290
3455
  catch (e) {
3291
- fullLibrary += "*Brand guidelines not found*\n";
3456
+ // Use fallback
3292
3457
  }
3293
- // Add CSS theme
3294
- fullLibrary += "\n\n---\n\n## 2. CSS Theme (globals.css)\n\n```css\n";
3458
+ contentBlocks.push({
3459
+ type: "text",
3460
+ text: `## 1. Brand Guidelines\n\n${guidelines}`
3461
+ });
3462
+ // Section 3: CSS Theme (with filename hint)
3463
+ let cssTheme = "/* CSS theme not found */";
3295
3464
  try {
3296
- fullLibrary += fs.readFileSync(getAssetPath("css"), "utf-8");
3465
+ cssTheme = fs.readFileSync(getAssetPath("css"), "utf-8");
3297
3466
  }
3298
3467
  catch (e) {
3299
- fullLibrary += "/* CSS theme not found */";
3468
+ // Use fallback
3300
3469
  }
3301
- fullLibrary += "\n```\n";
3302
- // Add utilities
3303
- fullLibrary += "\n\n---\n\n## 3. Utilities (lib/utils.ts)\n\n```typescript\n";
3470
+ contentBlocks.push({
3471
+ type: "text",
3472
+ text: `## 2. CSS Theme
3473
+
3474
+ ${formatCodeBlock(cssTheme, { language: "css", filename: "globals.css" })}`
3475
+ });
3476
+ // Section 4: Utilities (with filename hint)
3477
+ let utils = "// Utils not found";
3304
3478
  try {
3305
- fullLibrary += fs.readFileSync(getAssetPath("utils"), "utf-8");
3479
+ utils = fs.readFileSync(getAssetPath("utils"), "utf-8");
3306
3480
  }
3307
3481
  catch (e) {
3308
- fullLibrary += "// Utils not found";
3482
+ // Use fallback
3309
3483
  }
3310
- fullLibrary += "\n```\n";
3311
- // Add components
3312
- fullLibrary += "\n\n---\n\n## 4. UI Components\n\n";
3484
+ contentBlocks.push({
3485
+ type: "text",
3486
+ text: `## 3. Utilities
3487
+
3488
+ ${formatCodeBlock(utils, { language: "typescript", filename: "lib/utils.ts" })}`
3489
+ });
3490
+ // Section 5: Components - each as a separate block
3491
+ contentBlocks.push({
3492
+ type: "text",
3493
+ text: `## 4. UI Components`
3494
+ });
3313
3495
  try {
3314
3496
  const componentsDir = getAssetPath("components");
3315
3497
  const files = fs.readdirSync(componentsDir)
@@ -3318,24 +3500,35 @@ ${!validation.valid ? `
3318
3500
  for (const file of files) {
3319
3501
  const componentName = file.replace('.tsx', '');
3320
3502
  const content = fs.readFileSync(path.join(componentsDir, file), "utf-8");
3321
- fullLibrary += `### ${componentName}\n\nFile: \`components/ui/${file}\`\n\n\`\`\`tsx\n${content}\n\`\`\`\n\n`;
3503
+ contentBlocks.push({
3504
+ type: "text",
3505
+ text: `### ${componentName}
3506
+
3507
+ ${formatCodeBlock(content, { language: "tsx", filename: `components/ui/${file}` })}`
3508
+ });
3322
3509
  }
3323
3510
  }
3324
3511
  catch (e) {
3325
- fullLibrary += "*Components directory not found*\n";
3512
+ contentBlocks.push({
3513
+ type: "text",
3514
+ text: "*Components directory not found*"
3515
+ });
3326
3516
  }
3327
- // Add logo section with implementation guidance
3328
- fullLibrary += "\n\n---\n\n## 5. Brand Logos\n\n";
3329
- fullLibrary += "Use these logos in your application. The Sonance+James+IPORT lockup is the default.\n\n";
3330
- fullLibrary += `### Logo Paths
3517
+ // Section 6: Logo guidance
3518
+ contentBlocks.push({
3519
+ type: "text",
3520
+ text: `## 5. Brand Logos
3521
+
3522
+ Use these logos in your application. The Sonance+James+IPORT lockup is the default.
3523
+
3524
+ ### Logo Paths
3331
3525
  | Variant | Path | Use On |
3332
3526
  |---------|------|--------|
3333
3527
  | **Light Backgrounds** | \`/logos/sonance-james-iport/Sonance_James_IPORT_Lockup_Dark.png\` | White, light gray backgrounds |
3334
3528
  | **Dark Backgrounds** | \`/logos/sonance-james-iport/Sonance_James_IPORT_Lockup_Light.png\` | Charcoal, dark backgrounds |
3335
3529
 
3336
3530
  ### Implementation (Next.js)
3337
- \`\`\`tsx
3338
- import Image from 'next/image';
3531
+ ${formatCodeBlock(`import Image from 'next/image';
3339
3532
 
3340
3533
  // Light mode logo (for light backgrounds)
3341
3534
  <Image
@@ -3355,8 +3548,7 @@ import Image from 'next/image';
3355
3548
  height={40}
3356
3549
  className="h-10 w-auto hidden dark:block"
3357
3550
  priority
3358
- />
3359
- \`\`\`
3551
+ />`, { language: "tsx", filename: "components/Logo.tsx" })}
3360
3552
 
3361
3553
  ### Sizing Guidelines
3362
3554
  - Header/Navbar: \`h-8\` to \`h-10\` (32-40px)
@@ -3364,18 +3556,8 @@ import Image from 'next/image';
3364
3556
  - Footer: \`h-6\` to \`h-8\` (24-32px)
3365
3557
 
3366
3558
  ### Other Brand Logos
3367
- Use \`list_logos\` to see all available logos, or \`get_logo\` to retrieve a specific logo with implementation code.
3368
- `;
3369
- // Build content blocks with embedded logos
3370
- const contentBlocks = [];
3371
- // Validate and embed logos
3372
- validateLogoMap();
3373
- const lightLogoPath = LOGO_MAP.default.light;
3374
- const darkLogoPath = LOGO_MAP.default.dark;
3375
- await embedLogoWithFeedback(lightLogoPath, "Brand Logo (for Light Backgrounds)", contentBlocks);
3376
- await embedLogoWithFeedback(darkLogoPath, "Brand Logo (for Dark Backgrounds)", contentBlocks);
3377
- // Add the full library text
3378
- contentBlocks.push({ type: "text", text: fullLibrary });
3559
+ Use \`list_logos\` to see all available logos, or \`get_logo\` to retrieve a specific logo with implementation code.`
3560
+ });
3379
3561
  return {
3380
3562
  content: contentBlocks,
3381
3563
  };
@@ -3942,6 +4124,28 @@ Now design the **${component_description}** following these tokens and principle
3942
4124
  // Single-theme mode: embed the appropriate logo
3943
4125
  await embedLogoWithFeedback(logoPath, `Brand Logo (${theme === "light" ? "for Light Backgrounds" : "for Dark Backgrounds"})`, contentBlocks);
3944
4126
  }
4127
+ // Add critical logo usage warning
4128
+ contentBlocks.push({
4129
+ type: "text",
4130
+ text: `## Logo Usage Warning
4131
+
4132
+ > **CRITICAL:** The logo image(s) shown above are the ONLY acceptable representations.
4133
+ >
4134
+ > **NEVER:**
4135
+ > - Draw the logo with SVG paths or shapes
4136
+ > - Recreate it with styled text or colored letters
4137
+ > - Approximate it with CSS or Unicode characters
4138
+ >
4139
+ > **ALWAYS:**
4140
+ > - Use the embedded image directly in your artifact
4141
+ > - Or use \`get_logo_base64\` for a ready-to-use data URL
4142
+ > - If you cannot embed the image, use placeholder text: "[Logo: ${brand.charAt(0).toUpperCase() + brand.slice(1)}]"
4143
+
4144
+ To get the logo as a base64 data URL for artifacts, use:
4145
+ \`\`\`
4146
+ get_logo_base64({ logo_path: "${logoPath}" })
4147
+ \`\`\``,
4148
+ });
3945
4149
  // Add the main response text
3946
4150
  contentBlocks.push({ type: "text", text: response });
3947
4151
  return {
@@ -4979,6 +5183,23 @@ ${palette.preferredTheme === "dark" ? `- **Note**: ${palette.name} prefers dark
4979
5183
  type: "text",
4980
5184
  text: `## Logo Usage\n\nTo add the logo to your document:\n1. Save the appropriate logo image above (right-click → Save as)\n2. Use the saved path in your code:\n\n**Word (python-docx):**\n\`\`\`python\ndoc.add_picture('logo_dark.png', width=Inches(2)) # For light backgrounds\n\`\`\`\n\n**PDF (ReportLab):**\n\`\`\`python\ncontent.append(Image('logo_dark.png', width=2*inch, height=0.5*inch))\n\`\`\`\n`,
4981
5185
  });
5186
+ // Add critical logo usage warning
5187
+ templateContentBlocks.push({
5188
+ type: "text",
5189
+ text: `## Logo Warning
5190
+
5191
+ > **CRITICAL:** Use ONLY the actual logo image files shown above.
5192
+ >
5193
+ > **NEVER:**
5194
+ > - Draw the logo with shapes or vector tools in your document
5195
+ > - Recreate it with styled text or colored letters
5196
+ > - Use any visual approximation or placeholder
5197
+ >
5198
+ > **ALWAYS:**
5199
+ > - Save and embed the actual PNG image file
5200
+ > - Use the code snippets above that reference the saved image file
5201
+ > - The logo images are embedded above - save them directly`,
5202
+ });
4982
5203
  // Add the main template text
4983
5204
  templateContentBlocks.push({ type: "text", text: template });
4984
5205
  return {
@@ -5270,6 +5491,23 @@ ${rebrandArgs.source_format === "word" ? `
5270
5491
  text: `> **Logo Note:** Brand logos could not be embedded automatically. Download the appropriate ${palette.name} logo from your brand assets folder or use the \`get_logo\` tool.`,
5271
5492
  });
5272
5493
  }
5494
+ // Add critical logo usage warning
5495
+ rebrandContentBlocks.push({
5496
+ type: "text",
5497
+ text: `## Logo Usage Warning
5498
+
5499
+ > **CRITICAL:** When adding the logo to your document, use the ACTUAL image files shown above.
5500
+ >
5501
+ > **NEVER:**
5502
+ > - Draw the logo with shapes or vector tools
5503
+ > - Recreate it with styled text or colored letters
5504
+ > - Use any visual approximation
5505
+ >
5506
+ > **ALWAYS:**
5507
+ > - Download and embed the actual logo image
5508
+ > - Or use \`get_logo_base64\` for a data URL for web/HTML
5509
+ > - For Word/PDF: Save the image above and use it directly`,
5510
+ });
5273
5511
  // Add the main guide text
5274
5512
  rebrandContentBlocks.push({ type: "text", text: rebrandGuide });
5275
5513
  return {
@@ -5380,7 +5618,26 @@ npm install @radix-ui/react-slot @radix-ui/react-dialog
5380
5618
 
5381
5619
  4. Copy the CSS and component files as described below
5382
5620
  `;
5383
- const response = `# ${palette.name} App Starter Kit
5621
+ // Build structured response with multiple content blocks for artifact rendering
5622
+ const contentBlocks = [];
5623
+ // Validate logo map on first use
5624
+ validateLogoMap();
5625
+ // Map brand to centralized LOGO_MAP (sonance uses default lockup)
5626
+ const brandLogoKey = brand === "sonance" ? "default" : brand;
5627
+ const lightLogoPath = LOGO_MAP[brandLogoKey]?.light || LOGO_MAP.default.light;
5628
+ const darkLogoPath = LOGO_MAP[brandLogoKey]?.dark || LOGO_MAP.default.dark;
5629
+ const lightEmbedded = await embedLogoWithFeedback(lightLogoPath, "Logo for Light Backgrounds", contentBlocks);
5630
+ const darkEmbedded = await embedLogoWithFeedback(darkLogoPath, "Logo for Dark Backgrounds", contentBlocks);
5631
+ if (!lightEmbedded && !darkEmbedded) {
5632
+ contentBlocks.push({
5633
+ type: "text",
5634
+ text: `> **Logo Note:** Brand logos could not be embedded. Use \`list_logos\` and \`get_logo\` to retrieve logo assets manually.`,
5635
+ });
5636
+ }
5637
+ // Section 1: Overview and Setup Instructions
5638
+ contentBlocks.push({
5639
+ type: "text",
5640
+ text: `# ${palette.name} App Starter Kit
5384
5641
 
5385
5642
  **App Type:** ${appType}
5386
5643
  **Framework:** ${framework}
@@ -5390,37 +5647,39 @@ npm install @radix-ui/react-slot @radix-ui/react-dialog
5390
5647
 
5391
5648
  ---
5392
5649
 
5393
- ${setupInstructions}
5394
-
5395
- ---
5396
-
5397
- ## Global Styles (copy to globals.css)
5398
-
5399
- \`\`\`css
5400
- ${cssTheme}
5401
- \`\`\`
5402
-
5403
- ---
5404
-
5405
- ## Utility Functions (copy to lib/utils.ts)
5406
-
5407
- \`\`\`typescript
5408
- ${utils}
5409
- \`\`\`
5410
-
5411
- ---
5412
-
5413
- ## Required Components (${componentSources.length} total)
5414
-
5415
- ${componentSources.map(c => `### ${c.name}.tsx
5650
+ ${setupInstructions}`
5651
+ });
5652
+ // Section 2: Global Styles (with filename hint for artifact rendering)
5653
+ contentBlocks.push({
5654
+ type: "text",
5655
+ text: `## Global Styles
5416
5656
 
5417
- \`\`\`tsx
5418
- ${c.source}
5419
- \`\`\``).join("\n\n")}
5657
+ ${formatCodeBlock(cssTheme, { language: "css", filename: "globals.css" })}`
5658
+ });
5659
+ // Section 3: Utility Functions (with filename hint)
5660
+ contentBlocks.push({
5661
+ type: "text",
5662
+ text: `## Utility Functions
5420
5663
 
5421
- ---
5664
+ ${formatCodeBlock(utils, { language: "typescript", filename: "lib/utils.ts" })}`
5665
+ });
5666
+ // Section 4: Each component as a separate content block
5667
+ contentBlocks.push({
5668
+ type: "text",
5669
+ text: `## Required Components (${componentSources.length} total)`
5670
+ });
5671
+ for (const comp of componentSources) {
5672
+ contentBlocks.push({
5673
+ type: "text",
5674
+ text: `### ${comp.name}.tsx
5422
5675
 
5423
- ## Page Template: ${appType}
5676
+ ${formatCodeBlock(comp.source, { language: "tsx", filename: `components/ui/${comp.name}.tsx` })}`
5677
+ });
5678
+ }
5679
+ // Section 5: Page Template
5680
+ contentBlocks.push({
5681
+ type: "text",
5682
+ text: `## Page Template: ${appType}
5424
5683
 
5425
5684
  ${layout.description}
5426
5685
 
@@ -5430,13 +5689,12 @@ ${layout.structure}
5430
5689
  - Grid: \`${layout.gridLayout}\`
5431
5690
  - Spacing: \`${layout.spacing}\`
5432
5691
 
5433
- \`\`\`tsx
5434
- ${layout.template}
5435
- \`\`\`
5436
-
5437
- ---
5438
-
5439
- ## Quick Reference
5692
+ ${formatCodeBlock(layout.template, { language: "tsx", filename: "app/page.tsx" })}`
5693
+ });
5694
+ // Section 6: Quick Reference
5695
+ contentBlocks.push({
5696
+ type: "text",
5697
+ text: `## Quick Reference
5440
5698
 
5441
5699
  ### ${palette.name} Brand Colors
5442
5700
  - **Primary:** ${palette.primary} (bg-primary, text-primary)
@@ -5461,25 +5719,8 @@ ${layout.template}
5461
5719
 
5462
5720
  ---
5463
5721
 
5464
- Now you have everything needed to build a ${palette.name}-branded ${appType} application!
5465
- `;
5466
- // Embed logos using centralized LOGO_MAP with proper error feedback
5467
- const contentBlocks = [];
5468
- // Validate logo map on first use
5469
- validateLogoMap();
5470
- // Map brand to centralized LOGO_MAP (sonance uses default lockup)
5471
- const brandLogoKey = brand === "sonance" ? "default" : brand;
5472
- const lightLogoPath = LOGO_MAP[brandLogoKey]?.light || LOGO_MAP.default.light;
5473
- const darkLogoPath = LOGO_MAP[brandLogoKey]?.dark || LOGO_MAP.default.dark;
5474
- const lightEmbedded = await embedLogoWithFeedback(lightLogoPath, "Logo for Light Backgrounds", contentBlocks);
5475
- const darkEmbedded = await embedLogoWithFeedback(darkLogoPath, "Logo for Dark Backgrounds", contentBlocks);
5476
- if (!lightEmbedded && !darkEmbedded) {
5477
- contentBlocks.push({
5478
- type: "text",
5479
- text: `> **Logo Note:** Brand logos could not be embedded. Use \`list_logos\` and \`get_logo\` to retrieve logo assets manually.`,
5480
- });
5481
- }
5482
- contentBlocks.push({ type: "text", text: response });
5722
+ Now you have everything needed to build a ${palette.name}-branded ${appType} application!`
5723
+ });
5483
5724
  return {
5484
5725
  content: contentBlocks,
5485
5726
  };
@@ -5957,31 +6198,49 @@ Use \`redesign_app\` tool on individual files for detailed transformation instru
5957
6198
  }
5958
6199
  const components = COMPONENT_CATEGORIES[category];
5959
6200
  const componentsDir = getAssetPath("components");
5960
- let response = `# ${category.charAt(0).toUpperCase() + category.slice(1)} Components (${components.length} components)\n\n`;
5961
- response += `Use these components for ${category === "forms" ? "building forms and capturing user input" :
6201
+ // Build structured response with multiple content blocks
6202
+ const contentBlocks = [];
6203
+ const categoryDescription = category === "forms" ? "building forms and capturing user input" :
5962
6204
  category === "navigation" ? "navigation and menus" :
5963
6205
  category === "feedback" ? "user feedback and notifications" :
5964
6206
  category === "overlays" ? "modals, dialogs, and overlays" :
5965
6207
  category === "data-display" ? "displaying data and content" :
5966
6208
  category === "layout" ? "page structure and layouts" :
5967
- "utility purposes"}.\n\n---\n\n`;
6209
+ "utility purposes";
6210
+ contentBlocks.push({
6211
+ type: "text",
6212
+ text: `# ${category.charAt(0).toUpperCase() + category.slice(1)} Components (${components.length} components)
6213
+
6214
+ Use these components for ${categoryDescription}.`
6215
+ });
5968
6216
  for (const compName of components) {
5969
6217
  const filePath = path.join(componentsDir, `${compName}.tsx`);
5970
6218
  if (fs.existsSync(filePath)) {
5971
6219
  try {
5972
6220
  const content = fs.readFileSync(filePath, "utf-8");
5973
- response += `## ${compName}\n\nFile: \`components/ui/${compName}.tsx\`\n\n\`\`\`tsx\n${content}\n\`\`\`\n\n---\n\n`;
6221
+ contentBlocks.push({
6222
+ type: "text",
6223
+ text: `## ${compName}
6224
+
6225
+ ${formatCodeBlock(content, { language: "tsx", filename: `components/ui/${compName}.tsx` })}`
6226
+ });
5974
6227
  }
5975
6228
  catch {
5976
- response += `## ${compName}\n\n*Could not read component file*\n\n---\n\n`;
6229
+ contentBlocks.push({
6230
+ type: "text",
6231
+ text: `## ${compName}\n\n*Could not read component file*`
6232
+ });
5977
6233
  }
5978
6234
  }
5979
6235
  else {
5980
- response += `## ${compName}\n\n*Component file not found*\n\n---\n\n`;
6236
+ contentBlocks.push({
6237
+ type: "text",
6238
+ text: `## ${compName}\n\n*Component file not found*`
6239
+ });
5981
6240
  }
5982
6241
  }
5983
6242
  return {
5984
- content: [{ type: "text", text: response }],
6243
+ content: contentBlocks,
5985
6244
  };
5986
6245
  }
5987
6246
  default:
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Response formatter utilities for artifact-friendly MCP outputs
3
+ *
4
+ * These utilities help format code blocks with proper language identifiers
5
+ * and filename hints that Claude Desktop can render as artifacts.
6
+ */
7
+ export interface CodeBlockOptions {
8
+ language: string;
9
+ filename?: string;
10
+ }
11
+ export interface MultiFileSection {
12
+ filename: string;
13
+ content: string;
14
+ language: string;
15
+ description?: string;
16
+ }
17
+ /**
18
+ * Format a code block with proper language identifier and optional filename hint
19
+ *
20
+ * @example
21
+ * formatCodeBlock(cssContent, { language: 'css', filename: 'globals.css' })
22
+ * // Returns: ```css:globals.css\n{content}\n```
23
+ */
24
+ export declare function formatCodeBlock(code: string, options: CodeBlockOptions): string;
25
+ /**
26
+ * Format multiple files as separate code blocks with headers
27
+ *
28
+ * @example
29
+ * formatMultiFile([
30
+ * { filename: 'button.tsx', content: '...', language: 'tsx' },
31
+ * { filename: 'card.tsx', content: '...', language: 'tsx' }
32
+ * ])
33
+ */
34
+ export declare function formatMultiFile(sections: MultiFileSection[]): string;
35
+ /**
36
+ * Auto-detect the language of code content
37
+ *
38
+ * @example
39
+ * detectLanguage('import React from "react"') // returns 'tsx'
40
+ * detectLanguage(':root { --color: red; }') // returns 'css'
41
+ */
42
+ export declare function detectLanguage(content: string): string;
43
+ /**
44
+ * Split large content into manageable chunks based on natural boundaries
45
+ *
46
+ * @param content - The content to split
47
+ * @param options - Split options
48
+ * @returns Array of content chunks
49
+ */
50
+ export declare function splitContent(content: string, options?: {
51
+ maxLines?: number;
52
+ splitOn?: RegExp;
53
+ }): string[];
54
+ /**
55
+ * Generate a table of contents from markdown content
56
+ *
57
+ * @param content - Markdown content with headings
58
+ * @returns Markdown-formatted table of contents
59
+ */
60
+ export declare function generateTOC(content: string): string;
61
+ /**
62
+ * Create a section with header and formatted code block
63
+ *
64
+ * @example
65
+ * createCodeSection('Global Styles', cssContent, { language: 'css', filename: 'globals.css' })
66
+ */
67
+ export declare function createCodeSection(title: string, code: string, options: CodeBlockOptions): string;
68
+ /**
69
+ * Get file extension to language mapping
70
+ */
71
+ export declare function extensionToLanguage(ext: string): string;
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Response formatter utilities for artifact-friendly MCP outputs
3
+ *
4
+ * These utilities help format code blocks with proper language identifiers
5
+ * and filename hints that Claude Desktop can render as artifacts.
6
+ */
7
+ /**
8
+ * Format a code block with proper language identifier and optional filename hint
9
+ *
10
+ * @example
11
+ * formatCodeBlock(cssContent, { language: 'css', filename: 'globals.css' })
12
+ * // Returns: ```css:globals.css\n{content}\n```
13
+ */
14
+ export function formatCodeBlock(code, options) {
15
+ const { language, filename } = options;
16
+ const header = filename ? `${language}:${filename}` : language;
17
+ return `\`\`\`${header}\n${code}\n\`\`\``;
18
+ }
19
+ /**
20
+ * Format multiple files as separate code blocks with headers
21
+ *
22
+ * @example
23
+ * formatMultiFile([
24
+ * { filename: 'button.tsx', content: '...', language: 'tsx' },
25
+ * { filename: 'card.tsx', content: '...', language: 'tsx' }
26
+ * ])
27
+ */
28
+ export function formatMultiFile(sections) {
29
+ return sections.map(section => {
30
+ const header = section.description
31
+ ? `### ${section.filename}\n\n${section.description}\n\n`
32
+ : `### ${section.filename}\n\n`;
33
+ return header + formatCodeBlock(section.content, {
34
+ language: section.language,
35
+ filename: section.filename
36
+ });
37
+ }).join('\n\n---\n\n');
38
+ }
39
+ /**
40
+ * Auto-detect the language of code content
41
+ *
42
+ * @example
43
+ * detectLanguage('import React from "react"') // returns 'tsx'
44
+ * detectLanguage(':root { --color: red; }') // returns 'css'
45
+ */
46
+ export function detectLanguage(content) {
47
+ // React/TSX patterns (check first - most specific)
48
+ if (/^['"]use client['"]|import.*from\s+['"]react['"]|<[A-Z][a-zA-Z]*[\s/>]/m.test(content)) {
49
+ return 'tsx';
50
+ }
51
+ // CSS patterns
52
+ if (/^:root\s*\{|@tailwind|@layer|--[a-z-]+:|@import\s+['"]|@media\s*\(/m.test(content)) {
53
+ return 'css';
54
+ }
55
+ // TypeScript patterns (non-React)
56
+ if (/^interface\s+\w+|^type\s+\w+\s*=|^export\s+type\s+|:\s*(string|number|boolean|void)\s*[;,)]/m.test(content)) {
57
+ return 'typescript';
58
+ }
59
+ // JSON patterns
60
+ if (/^\s*\{[\s\S]*"[^"]+"\s*:/m.test(content)) {
61
+ return 'json';
62
+ }
63
+ // Python patterns
64
+ if (/^from\s+\w+\s+import|^import\s+\w+|^def\s+\w+\s*\(|^class\s+\w+(\s*\(|:)/m.test(content)) {
65
+ return 'python';
66
+ }
67
+ // Bash/Shell patterns
68
+ if (/^#!/m.test(content) || /^\s*(npm|npx|yarn|pnpm|git|cd|mkdir|curl|wget)\s/m.test(content)) {
69
+ return 'bash';
70
+ }
71
+ // HTML patterns
72
+ if (/^<!DOCTYPE|^<html|^<head|^<body/im.test(content)) {
73
+ return 'html';
74
+ }
75
+ // Markdown patterns
76
+ if (/^#{1,6}\s+|^\*{1,2}[^*]|^\[.*\]\(.*\)|^>\s+/m.test(content)) {
77
+ return 'markdown';
78
+ }
79
+ return 'text';
80
+ }
81
+ /**
82
+ * Split large content into manageable chunks based on natural boundaries
83
+ *
84
+ * @param content - The content to split
85
+ * @param options - Split options
86
+ * @returns Array of content chunks
87
+ */
88
+ export function splitContent(content, options = {}) {
89
+ const { maxLines = 200, splitOn = /^(?=#{1,3}\s)/m } = options;
90
+ const lines = content.split('\n');
91
+ if (lines.length <= maxLines) {
92
+ return [content];
93
+ }
94
+ // Try to split on natural boundaries (headers)
95
+ const sections = content.split(splitOn).filter(Boolean);
96
+ if (sections.length > 1) {
97
+ return sections;
98
+ }
99
+ // Fall back to line-based splitting
100
+ const chunks = [];
101
+ for (let i = 0; i < lines.length; i += maxLines) {
102
+ chunks.push(lines.slice(i, i + maxLines).join('\n'));
103
+ }
104
+ return chunks;
105
+ }
106
+ /**
107
+ * Generate a table of contents from markdown content
108
+ *
109
+ * @param content - Markdown content with headings
110
+ * @returns Markdown-formatted table of contents
111
+ */
112
+ export function generateTOC(content) {
113
+ const headings = content.match(/^#{1,3}\s+.+$/gm) || [];
114
+ return headings.map(heading => {
115
+ const level = (heading.match(/^#+/) || [''])[0].length;
116
+ const text = heading.replace(/^#+\s+/, '');
117
+ const anchor = text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-+$/, '');
118
+ const indent = ' '.repeat(level - 1);
119
+ return `${indent}- [${text}](#${anchor})`;
120
+ }).join('\n');
121
+ }
122
+ /**
123
+ * Create a section with header and formatted code block
124
+ *
125
+ * @example
126
+ * createCodeSection('Global Styles', cssContent, { language: 'css', filename: 'globals.css' })
127
+ */
128
+ export function createCodeSection(title, code, options) {
129
+ return `## ${title}\n\n${formatCodeBlock(code, options)}`;
130
+ }
131
+ /**
132
+ * Get file extension to language mapping
133
+ */
134
+ export function extensionToLanguage(ext) {
135
+ const map = {
136
+ '.tsx': 'tsx',
137
+ '.ts': 'typescript',
138
+ '.jsx': 'tsx',
139
+ '.js': 'javascript',
140
+ '.css': 'css',
141
+ '.scss': 'scss',
142
+ '.json': 'json',
143
+ '.md': 'markdown',
144
+ '.py': 'python',
145
+ '.html': 'html',
146
+ '.sh': 'bash',
147
+ '.bash': 'bash',
148
+ '.yaml': 'yaml',
149
+ '.yml': 'yaml',
150
+ };
151
+ return map[ext.toLowerCase()] || 'text';
152
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.116",
3
+ "version": "1.3.118",
4
4
  "description": "MCP Server for Sonance Brand Guidelines and Component Library - gives Claude instant access to brand colors, typography, and UI components.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",