miromiro-mcp 0.6.0 → 0.7.0

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.
Files changed (3) hide show
  1. package/README.md +17 -0
  2. package/package.json +1 -1
  3. package/src/index.mjs +141 -44
package/README.md CHANGED
@@ -78,13 +78,30 @@ claude mcp add miromiro -s user \
78
78
 
79
79
  All tools are read-only and retry-safe. Identical calls within 24 hours are served from cache for free, and failed calls are never billed.
80
80
 
81
+ ### Asking for a part of the page by name
82
+
83
+ `extract_code` takes `section` - one of `hero`, `header`, `footer`, `pricing`, `features`, `testimonials`, `faq`, `cta` - so the agent never has to invent a CSS selector for a page it cannot see. Every result also lists the page's other `sections` with ready selectors for follow-up calls. A selector or section that matches nothing falls back to the hero with a `note` saying what the page does have, instead of a dead-end 404.
84
+
85
+ ### Prompts (slash commands)
86
+
87
+ The server exposes three MCP prompts, which clients surface before the first call - Claude Code as `/mcp__miromiro__clone_section`, `/mcp__miromiro__brand_kit` and `/mcp__miromiro__design_tokens`, VS Code as `/miromiro.<name>`, Cursor in its prompt menu:
88
+
89
+ | Prompt | Arguments | What it does |
90
+ |---|---|---|
91
+ | `clone_section` | `url`, `section?`, `framework?` | Rebuild a section of a live site as paste-ready code, with its real values, and list the other sections |
92
+ | `brand_kit` | `url` | The brand colors, fonts, logo and socials, explained (declared vs inferred) |
93
+ | `design_tokens` | `url`, `format?` | The complete token system as CSS, Tailwind, DTCG or JSON, with a short summary |
94
+
81
95
  ### Controlling output size (remote server)
82
96
 
83
97
  Remote results are trimmed by default so a big page cannot flood the chat context, and every trimmed response says exactly what was dropped. Two optional arguments loosen it per call:
84
98
 
99
+ - `response_format: 'concise' | 'detailed'` - the one-word knob: `concise` keeps lists to 10 entries for a chat answer, `detailed` returns everything the tool has
85
100
  - `limit` (up to 200) - raise the item cap: "give me 80 SVGs, not 20"
86
101
  - `full_output: true` - complete long strings, like entire inline SVG markup
87
102
 
103
+ Generated code is never trimmed. Results stop at roughly 20k tokens (25k for `extract_code`) so they fit the tool-result cap of Claude Code, Claude Desktop and Cursor, and the `truncation_note` says how many tokens were cut and which argument brings them back.
104
+
88
105
  The REST API and this local npm server always return everything untrimmed, so `limit` and
89
106
  `full_output` apply to the remote server only. Every tool on both servers accepts
90
107
  `render: true`, plus two more shared arguments:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miromiro-mcp",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "MCP server for the MiroMiro design-extraction API — give your coding agent the real code, design tokens, assets, SVGs, fonts, and Lottie of any website.",
5
5
  "mcpName": "app.miromiro/miromiro",
6
6
  "type": "module",
package/src/index.mjs CHANGED
@@ -16,7 +16,9 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
16
16
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
17
17
  import {
18
18
  CallToolRequestSchema,
19
- ListToolsRequestSchema
19
+ ListToolsRequestSchema,
20
+ ListPromptsRequestSchema,
21
+ GetPromptRequestSchema
20
22
  } from '@modelcontextprotocol/sdk/types.js'
21
23
 
22
24
  const API_BASE = process.env.MIROMIRO_API_BASE || 'https://miromiro.app/api/v1'
@@ -34,12 +36,16 @@ const TOOL_ANNOTATIONS = {
34
36
  openWorldHint: true
35
37
  }
36
38
 
39
+ // Sections an agent can ask for by name instead of inventing a CSS selector
40
+ // (mirrors NAMED_SECTIONS in the API's extractCode.ts).
41
+ const NAMED_SECTIONS = ['hero', 'header', 'footer', 'pricing', 'features', 'testimonials', 'faq', 'cta']
42
+
37
43
  // Shared input arg: every tool accepts render=true to execute the page's
38
44
  // JavaScript in a real (rented) browser before extraction - the fix for
39
45
  // client-rendered React/Next/SPA sites that return little statically.
40
46
  const RENDER_ARG = {
41
47
  type: 'boolean',
42
- description: 'Run the page in a real browser first so its JavaScript executes - use for client-rendered (React/Next/SPA) sites, or when a normal call came back thin. Slower, and adds 5 credits to the call.'
48
+ description: 'Run the page in a real browser first so its JavaScript executes - use for client-rendered (React/Next/SPA) sites, when a normal call came back thin, or after a 403 target_blocked (bot protection refuses plain HTTP requests but usually lets a browser through). Slower, and adds 5 credits to the call.'
43
49
  }
44
50
 
45
51
  // Shared input arg: skip the 24h cache for this one call (charged normally).
@@ -76,12 +82,13 @@ const TOOLS = [
76
82
  title: 'Extract section as code',
77
83
  endpoint: 'code',
78
84
  description:
79
- "Turn a section of a LIVE website into clean, paste-ready component code. Give it a URL and (optionally) a CSS selector; it resolves the page's real CSS cascade and returns a self-contained component in Tailwind, plain HTML+CSS, React (JSX) or Vue — plus the design tokens that section actually uses. The response also includes `sections`: every extractable section on the page with a human-readable label and a ready selector — when the user wants a specific part (pricing, footer, testimonials), find it there and call again with that selector. For jsx/vue, repeated structures (pricing tiers, testimonial cards, feature lists) are collapsed into a data array rendered with .map()/v-for - one template plus the data, like a human would write it. The section's ::before/::after decorations come back as real elements, dark-mode styles as dark: variants, and its @keyframes ship in the css field. The `theme` field (light | dark) tells you which theme the returned code actually is - measured from the page, never echoed back from your request. A site that expresses its themes through a prefers-color-scheme media query returns its LIGHT branch by default, so pass theme='dark' when the user is looking at that site in dark mode or asks for its dark version; sites that scope themes to a root attribute or class are read correctly either way. Say which theme you returned. USE THIS whenever the user asks to rebuild, clone, port, or copy a section of a real website: it returns the actual code with true colors, fonts, spacing and copy, so you adapt real values instead of guessing from a screenshot. Note: it reads static HTML/CSS by default - summary.styledCoverage (0-100) tells you how much styling resolved, and a client-rendered app that returns little includes a hint field saying to call again with render=true (+5 credits). Follow it.",
85
+ "Use when the user asks to rebuild, clone, copy, port, restyle or 'make it look like' part of a real website, or wants a site's section as Tailwind, HTML+CSS, React (JSX) or Vue. Give it the URL and, when the user named a part of the page, section (hero, header, footer, pricing, features, testimonials, faq, cta) - never invent a CSS selector. With neither, it auto-detects the hero; every call also returns `sections`, each other extractable section with a label and a ready selector for a follow-up call. Returns a self-contained component with the page's real resolved styles (true colors, fonts, spacing, copy) plus the tokens and assets it uses, so you adapt real values instead of estimating from a screenshot. For jsx/vue, repeated structures collapse into a data array rendered with .map()/v-for; ::before/::after come back as real elements; @keyframes ship in the css field. The `theme` field says which theme the code actually is - sites with a prefers-color-scheme dark variant return the light branch unless you pass theme='dark'; say which you returned. Not for: whole-page scraping, page text, or sites that need a login. Reads static HTML/CSS by default; if the result is thin or carries a `hint`, call again with render=true (+5 credits).",
80
86
  inputSchema: {
81
87
  type: 'object',
82
88
  properties: {
83
89
  url: { type: 'string', description: 'The website URL, e.g. stripe.com' },
84
- selector: { type: 'string', description: 'CSS selector for the section to extract, e.g. "#hero" or ".pricing-card". Omit to auto-detect the hero.' },
90
+ section: { type: 'string', enum: NAMED_SECTIONS, description: "Which part of the page, by name - use this when the user asked for the pricing, footer, header/nav, features, testimonials, FAQ or call-to-action block. Resolved on the page itself, so it works without seeing the markup. Omit for the hero. If the page has no such section you get the hero plus a `note` listing what is there." },
91
+ selector: { type: 'string', description: 'CSS selector for the section, ONLY when you have one from a previous result\'s `sections` list (e.g. "section.pricing-grid"). Do not guess selectors: a selector that matches nothing falls back to the hero with a `note`, and the guess costs a call. Prefer `section` for named parts of a page.' },
85
92
  framework: { type: 'string', enum: ['tailwind', 'html', 'jsx', 'vue'], description: 'The code to generate (default tailwind).' },
86
93
  name: { type: 'string', description: 'Component name for the jsx target (default ExtractedSection). Ignored by tailwind, html and vue. Renaming never re-runs the extraction - it costs 0 credits on a cached section.' },
87
94
  theme: { type: 'string', enum: ['light', 'dark'], description: "Which colour scheme to extract (default light). Pass 'dark' when the user is looking at the site in dark mode or asks for the dark version: the page's prefers-color-scheme: dark rules and custom properties become the component itself instead of riding along as dark: variants. Works whether the site expresses its themes as @media (prefers-color-scheme) or as a root attribute/class the CSS is scoped to ([data-theme=dark], html.dark). Sites that switch themes purely in JavaScript cannot be selected this way - you get the page default plus a hint saying so. No extra credits." },
@@ -94,7 +101,9 @@ const TOOLS = [
94
101
  properties: {
95
102
  url: { type: 'string' },
96
103
  selector: { type: 'string', description: 'The CSS selector that was extracted' },
104
+ section: { type: 'string', description: 'The named section that was resolved, when one was requested' },
97
105
  autoDetected: { type: 'boolean', description: 'True when the hero was auto-detected rather than passed in' },
106
+ note: { type: 'string', description: 'Present when the requested selector or section was not found and the hero was returned instead - says what was asked for and which sections the page does have. Tell the user.' },
98
107
  sections: {
99
108
  type: 'array',
100
109
  description: 'Every extractable section on the page - call again with one of these selectors for a specific part',
@@ -129,7 +138,7 @@ const TOOLS = [
129
138
  title: 'Extract brand identity',
130
139
  endpoint: 'brand',
131
140
  description:
132
- "Get a website's curated brand identity: color roles (primary, accent, background, text) read from the site's own CSS variables and logo — each as hex plus a human-readable name like 'Royal Blue' — dark/light theme detection, the brand palette, fonts split by role (primary/mono), logo, social image, the brand's social profiles (X, GitHub, LinkedIn, …), and a 0-1 confidence score for how much the site declared vs how much was inferred. Use this when you only need a site's STYLE (colors/fonts) rather than its code — to build new UI 'in the style of' a reference site. If you need the actual markup of a section, use extract_code instead. The result also carries `signals`, which explains WHERE the colors came from: check `signals.primarySource` and tell the user whether these are the site's own declared tokens ('css-var'/'theme-color') or values inferred from usage frequency ('usage') — never present inferred colors as the brand's official palette. `signals.explanation` is a sentence written to be repeated to the user.",
141
+ "Use when the user wants a site's brand: its colors, fonts, logo, theme or social links - 'what colors does stripe use', 'match this brand', 'build something in the style of X'. Returns role colors (primary, accent, background, text) as hex with human-readable names, the wider palette, fonts by role, logo candidates, social profiles, dark/light theme, and a 0-1 confidence score with `signals` explaining where the colors came from: read signals.explanation and tell the user whether they were declared by the site ('css-var'/'theme-color') or inferred from usage frequency ('usage', a guess) - never present inferred colors as the official palette. Not for: markup or layout (use extract_code) or the full token list (use extract_design_tokens).",
133
142
  inputSchema: {
134
143
  type: 'object',
135
144
  properties: { url: { type: 'string', description: 'The website URL, e.g. stripe.com' }, render: RENDER_ARG, fresh: FRESH_ARG, screenshot: SCREENSHOT_ARG },
@@ -186,7 +195,7 @@ const TOOLS = [
186
195
  title: 'Extract design tokens',
187
196
  endpoint: 'extract',
188
197
  description:
189
- "Extract the full design-token system of a URL: every color, font family, font size, border radius, box shadow, line height and z-index (with usage counts). Pass format='css' for a :root {} block, format='tailwind' for a tailwind.config, or format='dtcg' for a standalone W3C Design Tokens file (drops into Style Dictionary / Figma variables pipelines). Use when you need the complete token set, not just the brand summary. Returns VALUES and how often each is used - not layout, spacing composition or shadow stacks, and not which value belongs to which element: if the user asked you to rebuild or clone a section, use extract_code instead, which returns the real markup with the values already in place. Counts are tallied over the whole stylesheet, so a site shipping both light and dark themes returns the union of the two (see colorSchemes) - do not assume the most-used color is the background. The colorSchemes field tells you which themes the page declares; when it lists both, pick colours by role rather than by frequency, and use extract_code with theme=dark when you need one theme on its own.",
198
+ "Use when the user wants a site's complete token system - every color, font family, size, weight, spacing value, radius, shadow, line height, gradient and breakpoint with usage counts - or a paste-ready :root block (format='css'), tailwind.config (format='tailwind') or W3C Design Tokens file (format='dtcg', for Style Dictionary / Figma variables). Returns values and counts only, not layout or which element uses what: if the user asked to rebuild or clone a section, use extract_code, which returns the markup with the values in place. A site shipping light and dark themes returns the union of both (see colorSchemes) - pick colors by role, not by frequency, and use extract_code with theme=dark for one theme on its own. Not for: a short brand summary (use extract_brand).",
190
199
  inputSchema: {
191
200
  type: 'object',
192
201
  properties: {
@@ -226,7 +235,7 @@ const TOOLS = [
226
235
  title: 'Extract images',
227
236
  endpoint: 'images',
228
237
  description:
229
- "List every image on a page (img, srcset, picture, CSS background, favicon, og:image) as resolved URLs tagged by source. Use to recover imagery from a site or pull reference assets. Best-effort from static HTML: if images are mounted by JavaScript, call again with render=true (+5 credits).",
238
+ "Use when the user wants the images from a page - photos, illustrations, backgrounds, favicon, social image - as resolved URLs tagged by source (img, srcset, picture, CSS background, favicon, og). Not for: icons and vector logos (use extract_svgs) or fonts (use extract_fonts). If images are mounted by JavaScript, call again with render=true (+5 credits).",
230
239
  inputSchema: {
231
240
  type: 'object',
232
241
  properties: { url: { type: 'string', description: 'The website URL' }, render: RENDER_ARG, fresh: FRESH_ARG, screenshot: SCREENSHOT_ARG },
@@ -261,7 +270,7 @@ const TOOLS = [
261
270
  title: 'Extract SVGs',
262
271
  endpoint: 'svg',
263
272
  description:
264
- "Extract inline SVGs (as paste-ready code) and SVG file references from a page. Use to grab icons and vector logos. Best-effort from static HTML: if icons are mounted by JavaScript, call again with render=true (+5 credits).",
273
+ "Use when the user wants a site's icons or vector logos: inline SVGs as paste-ready markup plus external .svg file URLs. Not for: raster images (use extract_assets). If icons are mounted by JavaScript, call again with render=true (+5 credits).",
265
274
  inputSchema: {
266
275
  type: 'object',
267
276
  properties: { url: { type: 'string', description: 'The website URL' }, render: RENDER_ARG, fresh: FRESH_ARG, screenshot: SCREENSHOT_ARG },
@@ -296,7 +305,7 @@ const TOOLS = [
296
305
  title: 'Extract fonts',
297
306
  endpoint: 'fonts',
298
307
  description:
299
- "Detect the real typography a site loads by resolving its full CSS cascade: every @font-face file with family/weight/style/format, Google Fonts links, preloads, plus a grouped per-family summary (families). Pass fields='families' for just the summary. Use to match typography.",
308
+ "Use when the user asks which fonts a site uses or wants to match its typography: every @font-face file with family/weight/style/format, Google Fonts links and preloads, grouped per family in `families` (pass fields='families' for just that). On a type-foundry site the families served are demo cuts (subset, features stripped), flagged demo:true with a note - pass that on, they are not the usable typeface. Not for: colors (use extract_brand) or code (use extract_code).",
300
309
  inputSchema: {
301
310
  type: 'object',
302
311
  properties: {
@@ -335,12 +344,14 @@ const TOOLS = [
335
344
  source: { type: 'string' },
336
345
  weights: { type: 'array', items: { type: 'string' } },
337
346
  styles: { type: 'array', items: { type: 'string' } },
338
- files: { type: 'number' }
347
+ files: { type: 'number' },
348
+ demo: { type: 'boolean', description: 'The family name marks it a foundry demo/trial cut, not the real font' }
339
349
  }
340
350
  }
341
351
  },
342
352
  googleFontsLinks: { type: 'array', items: { type: 'string' } },
343
353
  count: { type: 'number' },
354
+ note: { type: 'string', description: 'Only present when demo/trial families were found - tell the user, these files are subset and feature-stripped, not the usable typeface' },
344
355
  usage: OUT_USAGE,
345
356
  truncation_note: OUT_TRUNCATION
346
357
  },
@@ -353,7 +364,7 @@ const TOOLS = [
353
364
  title: 'Extract Lottie animations',
354
365
  endpoint: 'lottie',
355
366
  description:
356
- "Find Lottie/animation JSON files declared on a page. Best-effort from static HTML - most Lotties are runtime-injected, so pass render=true to execute the page JS first and catch those too (+5 credits). Use to recover animations.",
367
+ "Use when the user wants a site's Lottie or animation JSON files. Most Lotties are injected at runtime, so pass render=true (+5 credits) when the static call finds none.",
357
368
  inputSchema: {
358
369
  type: 'object',
359
370
  properties: { url: { type: 'string', description: 'The website URL' }, render: RENDER_ARG, fresh: FRESH_ARG, screenshot: SCREENSHOT_ARG },
@@ -388,7 +399,7 @@ const TOOLS = [
388
399
  title: 'Extract videos',
389
400
  endpoint: 'videos',
390
401
  description:
391
- 'List every video a page declares: <video>/<source> files, og:video, player embeds (YouTube, Vimeo, Wistia, Loom) and direct video-file links - each tagged by source, with a poster image when declared. Note `source: "embed"` is a player page, not a downloadable file. Best-effort from static HTML: most sites mount their player with JavaScript, so if the result is thin or empty, call again with render=true to execute the page JS first (+5 credits).',
402
+ 'Use when the user wants the videos on a page: <video>/<source> files, og:video, YouTube/Vimeo/Wistia/Loom embeds and direct video links, each tagged by source with a poster when declared. `source: "embed"` is a player page, not a downloadable file. Most players mount with JavaScript: if the result is thin or empty, call again with render=true (+5 credits).',
392
403
  inputSchema: {
393
404
  type: 'object',
394
405
  properties: { url: { type: 'string', description: 'The website URL' }, render: RENDER_ARG, fresh: FRESH_ARG, screenshot: SCREENSHOT_ARG },
@@ -424,7 +435,7 @@ const TOOLS = [
424
435
  title: 'Extract audio',
425
436
  endpoint: 'audio',
426
437
  description:
427
- 'List every audio file a page declares: <audio>/<source> files, og:audio, and direct audio-file links such as podcast enclosures and download buttons - each tagged by source. Best-effort from static HTML: if a JavaScript player mounts the audio at runtime, call again with render=true (+5 credits).',
438
+ 'Use when the user wants the audio files on a page: <audio>/<source> files, og:audio, podcast enclosures and download links, each tagged by source. If a JavaScript player mounts the audio at runtime, call again with render=true (+5 credits).',
428
439
  inputSchema: {
429
440
  type: 'object',
430
441
  properties: { url: { type: 'string', description: 'The website URL' }, render: RENDER_ARG, fresh: FRESH_ARG, screenshot: SCREENSHOT_ARG },
@@ -458,15 +469,14 @@ const TOOLS = [
458
469
 
459
470
  // Query params a tool may forward to the API, beyond `url`. Anything else in the
460
471
  // tool arguments is ignored, so a hallucinated param can't reach the endpoint.
461
- const FORWARDED_PARAMS = ['format', 'selector', 'framework', 'name', 'theme', 'fields', 'render', 'fresh', 'screenshot']
472
+ const FORWARDED_PARAMS = ['format', 'selector', 'section', 'framework', 'name', 'theme', 'fields', 'render', 'fresh', 'screenshot']
462
473
 
463
- async function callApi(endpoint, args) {
464
- const q = new URLSearchParams({ url: args.url })
465
- for (const p of FORWARDED_PARAMS) {
466
- if (args[p] !== undefined && args[p] !== null && args[p] !== '') {
467
- q.set(p, String(args[p]))
468
- }
469
- }
474
+ // A selector/section that matched nothing. The API answers 404 (free); the
475
+ // stdio server then falls back to the hero so the agent gets the `sections`
476
+ // list in the same turn instead of a dead end - mirrors the remote server.
477
+ const SELECTOR_MISS_RE = /^No (element matched|"[a-z]+" section was found)/i
478
+
479
+ async function fetchApi(endpoint, q) {
470
480
  const res = await fetch(`${API_BASE}/${endpoint}?${q}`, {
471
481
  headers: { Authorization: `Bearer ${API_KEY}` }
472
482
  })
@@ -474,42 +484,129 @@ async function callApi(endpoint, args) {
474
484
  const body = isJson ? await res.json() : await res.text()
475
485
  if (!res.ok) {
476
486
  const msg = isJson ? (body.statusMessage || body.message || JSON.stringify(body)) : body
477
- throw new Error(`MiroMiro API ${res.status}: ${msg}`)
487
+ const err = new Error(`MiroMiro API ${res.status}: ${msg}`)
488
+ err.status = res.status
489
+ err.apiMessage = String(msg)
490
+ throw err
491
+ }
492
+ return { body, isJson }
493
+ }
494
+
495
+ async function callApi(endpoint, args) {
496
+ const q = new URLSearchParams({ url: args.url })
497
+ for (const p of FORWARDED_PARAMS) {
498
+ if (args[p] !== undefined && args[p] !== null && args[p] !== '') {
499
+ q.set(p, String(args[p]))
500
+ }
478
501
  }
502
+ let result
503
+ let note = null
504
+ try {
505
+ result = await fetchApi(endpoint, q)
506
+ } catch (err) {
507
+ const asked = q.get('selector') ? `selector "${q.get('selector')}"` : q.get('section') ? `section "${q.get('section')}"` : null
508
+ if (err.status !== 404 || !asked || !SELECTOR_MISS_RE.test(err.apiMessage || '')) throw err
509
+ q.delete('selector')
510
+ q.delete('section')
511
+ result = await fetchApi(endpoint, q)
512
+ note = `The requested ${asked} was not found on this page, so the auto-detected hero was returned instead. ` +
513
+ 'Pick the part the user wants from `sections` (call again with its selector) or pass section=' +
514
+ `${NAMED_SECTIONS.filter(s => s !== 'hero').join('|')}. Tell the user which section you returned.`
515
+ }
516
+ const { body, isJson } = result
479
517
  // Compact JSON: pretty-printing spends ~30% more model context on whitespace.
480
- return isJson ? JSON.stringify(body) : body
518
+ if (!isJson) return body
519
+ return JSON.stringify(note && body && typeof body === 'object' && !Array.isArray(body) ? { ...body, note } : body)
481
520
  }
482
521
 
522
+ // Prompts: the one MCP primitive a user sees BEFORE the first call. Claude Code
523
+ // lists them as /mcp__miromiro__<name>, VS Code as /miromiro.<name>, Cursor in
524
+ // its prompt menu. Same text as the remote server.
525
+ const PROMPT_URL_ARG = { name: 'url', description: 'The website URL, e.g. stripe.com', required: true }
526
+ const PROMPTS = [
527
+ {
528
+ name: 'clone_section',
529
+ title: 'Clone a section of a live site',
530
+ description: 'Rebuild a section of a real website (hero by default) as paste-ready code in the framework you use, with its real colors, fonts and spacing.',
531
+ arguments: [
532
+ PROMPT_URL_ARG,
533
+ { name: 'section', description: `Which part: ${NAMED_SECTIONS.join(', ')} (default hero)` },
534
+ { name: 'framework', description: 'tailwind (default), html, jsx or vue' }
535
+ ],
536
+ render: ({ url, section, framework }) =>
537
+ `Rebuild the ${section || 'hero'} section of ${url} as ${framework || 'tailwind'} code I can paste into my project. ` +
538
+ `Call extract_code with url="${url}"${section ? `, section="${section}"` : ''}${framework ? `, framework="${framework}"` : ''} ` +
539
+ 'and adapt the result: keep its real colors, fonts, spacing and copy, fix anything that is clearly page-specific, and tell me which theme (light or dark) the extraction returned. ' +
540
+ 'If the result is thin or carries a hint, retry with render=true before giving up. Then list the other sections the page has in case I want one of those next.'
541
+ },
542
+ {
543
+ name: 'brand_kit',
544
+ title: 'Brand kit from a URL',
545
+ description: "A site's brand identity - role colors with names, fonts, logo, socials - explained, not dumped, with a note on how much was declared vs inferred.",
546
+ arguments: [PROMPT_URL_ARG],
547
+ render: ({ url }) =>
548
+ `Give me the brand kit for ${url}: primary, accent, background and text colors (hex plus names), the fonts by role, the best logo URL and social profiles. ` +
549
+ `Call extract_brand with url="${url}". Read signals.explanation and tell me whether the colors were declared by the site or inferred from usage; if they were inferred, say so plainly. ` +
550
+ 'Finish with one line on what I could extract next (a section as code, or the full token set).'
551
+ },
552
+ {
553
+ name: 'design_tokens',
554
+ title: 'Design tokens from a URL',
555
+ description: "A site's complete token system as a paste-ready block: CSS custom properties, a Tailwind theme, or a W3C Design Tokens file.",
556
+ arguments: [
557
+ PROMPT_URL_ARG,
558
+ { name: 'format', description: 'css (default), tailwind, dtcg or json' }
559
+ ],
560
+ render: ({ url, format }) =>
561
+ `Extract the design tokens of ${url} as ${format || 'css'}. Call extract_design_tokens with url="${url}", format="${format || 'css'}". ` +
562
+ 'Return the block ready to paste, then summarise the system in a few lines: the palette by role, the type scale, the spacing and radius scale. ' +
563
+ 'If colorSchemes lists both light and dark, say that the values are the union of the two and that extract_code with theme=dark separates them.'
564
+ }
565
+ ]
566
+
483
567
  // Server-level guidance, sent once at initialize. Mirrors the `instructions`
484
568
  // string the remote server (miromiro.app/mcp) returns, so a stdio client gets
485
569
  // the same steering as a hosted one — chiefly: explain results, don't just dump
486
570
  // them, and never present inferred brand colors as declared ones.
487
571
  const INSTRUCTIONS =
488
- 'MiroMiro turns any live URL into design data: section-level code (extract_code), brand identity ' +
489
- '(extract_brand), the full design-token system (extract_design_tokens), images, SVGs, fonts, videos, audio and Lottie files. ' +
490
- 'Reach for extract_code when asked to rebuild or clone part of a real site; extract_brand when only ' +
491
- 'colors/fonts are needed; extract_design_tokens when you want the whole value system (it returns values ' +
492
- 'and usage counts, NOT layout, spacing composition or which value belongs to which element - it is not a ' +
493
- 'substitute for extract_code on a rebuild). When a site expresses its themes through a ' +
494
- 'prefers-color-scheme media query, extraction returns the LIGHT branch by default and carries the dark ' +
495
- 'one as dark: variants - so pass theme=dark to extract_code when the user is viewing that site in dark ' +
496
- 'mode or asks for its dark version. Every code response reports the theme you actually got: check it and ' +
497
- 'say which one you returned. The API reads static HTML+CSS by default; pass render=true on any tool to ' +
498
- 'execute the page JavaScript in a real browser first (client-rendered sites, +5 credits). When a static ' +
499
- 'call comes back thin because the page is client-rendered, the response includes a hint field saying to ' +
500
- 'retry with render=true - follow it. Explain your results, do not just dump them: extract_brand returns a ' +
501
- 'signals object saying where the colors came from (signals.explanation is a sentence written to be repeated ' +
502
- 'to the user) - when signals.primarySource is "usage" the site declared nothing and the colors are an ' +
503
- 'inferred guess, so say that instead of presenting them as the official palette. Rendered calls can also ' +
504
- 'take screenshot=true for a public image URL of the page (show it to the user to verify an extraction). ' +
505
- 'extract_design_tokens takes format=dtcg for a W3C Design Tokens file. Pass fresh=true on any tool to ' +
506
- 'bypass the 24h cache when a site just changed.'
572
+ 'Use MiroMiro when the user wants anything from a real, live website\'s design: rebuild, clone, copy or ' +
573
+ 'restyle a section, or "make mine look like X" (extract_code); its brand colors, fonts and logo (extract_brand); ' +
574
+ 'its complete design-token set as CSS variables or a W3C tokens file (extract_design_tokens); or its images, ' +
575
+ 'SVG icons, font files, videos, audio or Lottie files (the extract_* asset tools). Any public URL works, no setup. ' +
576
+ 'Not for: general web scraping or search, page text, pages behind a login, or CSS questions with no specific site. ' +
577
+ 'How to call: give extract_code the URL plus section=hero|header|footer|pricing|features|testimonials|faq|cta when ' +
578
+ 'the user named a part of the page; never invent a CSS selector (a miss falls back to the hero with a `note`). Every ' +
579
+ 'result lists the page\'s other `sections` with ready selectors for follow-ups. Static HTML+CSS is read by default: when a result is thin, carries a ' +
580
+ '`hint`, or a 403 target_blocked comes back, retry the SAME url with render=true (+5 credits) before telling the ' +
581
+ 'user a site cannot be extracted. Theme: a site with a prefers-color-scheme dark variant returns its light branch ' +
582
+ 'by default; pass theme=dark to extract_code for the dark version and report the `theme` field you actually got. ' +
583
+ 'extract_design_tokens returns values and counts, not layout - not a substitute for extract_code on a rebuild; ' +
584
+ 'format=dtcg gives a W3C Design Tokens file. Explain results, do not dump them: extract_brand\'s ' +
585
+ 'signals.explanation says whether the colors were declared by the site or inferred (primarySource "usage" = a ' +
586
+ 'guess) - repeat it to the user. Rendered calls accept screenshot=true for a public image URL of the page. ' +
587
+ 'fresh=true bypasses the 24h cache. Every result carries `usage` (credits left).'
507
588
 
508
589
  const server = new Server(
509
- { name: 'miromiro', version: '0.6.0' },
510
- { capabilities: { tools: {} }, instructions: INSTRUCTIONS }
590
+ { name: 'miromiro', version: '0.7.0' },
591
+ { capabilities: { tools: {}, prompts: {} }, instructions: INSTRUCTIONS }
511
592
  )
512
593
 
594
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
595
+ prompts: PROMPTS.map(({ name, title, description, arguments: args }) => ({ name, title, description, arguments: args }))
596
+ }))
597
+
598
+ server.setRequestHandler(GetPromptRequestSchema, async (req) => {
599
+ const prompt = PROMPTS.find(p => p.name === req.params.name)
600
+ if (!prompt) throw new Error(`Unknown prompt: ${req.params.name}`)
601
+ const args = req.params.arguments || {}
602
+ const missing = prompt.arguments.filter(a => a.required && !args[a.name])
603
+ if (missing.length) throw new Error(`Missing required argument: ${missing.map(a => a.name).join(', ')}`)
604
+ return {
605
+ description: prompt.description,
606
+ messages: [{ role: 'user', content: { type: 'text', text: prompt.render(args) } }]
607
+ }
608
+ })
609
+
513
610
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
514
611
  tools: TOOLS.map(({ name, title, description, inputSchema, outputSchema }) => ({
515
612
  name,