miromiro-mcp 0.6.1 → 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.
- package/README.md +17 -0
- package/package.json +1 -1
- package/src/index.mjs +137 -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.
|
|
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,6 +36,10 @@ 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.
|
|
@@ -76,12 +82,13 @@ const TOOLS = [
|
|
|
76
82
|
title: 'Extract section as code',
|
|
77
83
|
endpoint: 'code',
|
|
78
84
|
description:
|
|
79
|
-
"
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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: {
|
|
@@ -355,7 +364,7 @@ const TOOLS = [
|
|
|
355
364
|
title: 'Extract Lottie animations',
|
|
356
365
|
endpoint: 'lottie',
|
|
357
366
|
description:
|
|
358
|
-
"
|
|
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.",
|
|
359
368
|
inputSchema: {
|
|
360
369
|
type: 'object',
|
|
361
370
|
properties: { url: { type: 'string', description: 'The website URL' }, render: RENDER_ARG, fresh: FRESH_ARG, screenshot: SCREENSHOT_ARG },
|
|
@@ -390,7 +399,7 @@ const TOOLS = [
|
|
|
390
399
|
title: 'Extract videos',
|
|
391
400
|
endpoint: 'videos',
|
|
392
401
|
description:
|
|
393
|
-
'
|
|
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).',
|
|
394
403
|
inputSchema: {
|
|
395
404
|
type: 'object',
|
|
396
405
|
properties: { url: { type: 'string', description: 'The website URL' }, render: RENDER_ARG, fresh: FRESH_ARG, screenshot: SCREENSHOT_ARG },
|
|
@@ -426,7 +435,7 @@ const TOOLS = [
|
|
|
426
435
|
title: 'Extract audio',
|
|
427
436
|
endpoint: 'audio',
|
|
428
437
|
description:
|
|
429
|
-
'
|
|
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).',
|
|
430
439
|
inputSchema: {
|
|
431
440
|
type: 'object',
|
|
432
441
|
properties: { url: { type: 'string', description: 'The website URL' }, render: RENDER_ARG, fresh: FRESH_ARG, screenshot: SCREENSHOT_ARG },
|
|
@@ -460,15 +469,14 @@ const TOOLS = [
|
|
|
460
469
|
|
|
461
470
|
// Query params a tool may forward to the API, beyond `url`. Anything else in the
|
|
462
471
|
// tool arguments is ignored, so a hallucinated param can't reach the endpoint.
|
|
463
|
-
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']
|
|
464
473
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
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) {
|
|
472
480
|
const res = await fetch(`${API_BASE}/${endpoint}?${q}`, {
|
|
473
481
|
headers: { Authorization: `Bearer ${API_KEY}` }
|
|
474
482
|
})
|
|
@@ -476,44 +484,129 @@ async function callApi(endpoint, args) {
|
|
|
476
484
|
const body = isJson ? await res.json() : await res.text()
|
|
477
485
|
if (!res.ok) {
|
|
478
486
|
const msg = isJson ? (body.statusMessage || body.message || JSON.stringify(body)) : body
|
|
479
|
-
|
|
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
|
+
}
|
|
480
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
|
|
481
517
|
// Compact JSON: pretty-printing spends ~30% more model context on whitespace.
|
|
482
|
-
|
|
518
|
+
if (!isJson) return body
|
|
519
|
+
return JSON.stringify(note && body && typeof body === 'object' && !Array.isArray(body) ? { ...body, note } : body)
|
|
483
520
|
}
|
|
484
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
|
+
|
|
485
567
|
// Server-level guidance, sent once at initialize. Mirrors the `instructions`
|
|
486
568
|
// string the remote server (miromiro.app/mcp) returns, so a stdio client gets
|
|
487
569
|
// the same steering as a hosted one — chiefly: explain results, don't just dump
|
|
488
570
|
// them, and never present inferred brand colors as declared ones.
|
|
489
571
|
const INSTRUCTIONS =
|
|
490
|
-
'MiroMiro
|
|
491
|
-
'
|
|
492
|
-
'
|
|
493
|
-
'
|
|
494
|
-
'
|
|
495
|
-
'
|
|
496
|
-
'
|
|
497
|
-
'
|
|
498
|
-
'
|
|
499
|
-
'
|
|
500
|
-
'
|
|
501
|
-
'
|
|
502
|
-
'
|
|
503
|
-
|
|
504
|
-
'
|
|
505
|
-
'
|
|
506
|
-
'to the user) - when signals.primarySource is "usage" the site declared nothing and the colors are an ' +
|
|
507
|
-
'inferred guess, so say that instead of presenting them as the official palette. Rendered calls can also ' +
|
|
508
|
-
'take screenshot=true for a public image URL of the page (show it to the user to verify an extraction). ' +
|
|
509
|
-
'extract_design_tokens takes format=dtcg for a W3C Design Tokens file. Pass fresh=true on any tool to ' +
|
|
510
|
-
'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).'
|
|
511
588
|
|
|
512
589
|
const server = new Server(
|
|
513
|
-
{ name: 'miromiro', version: '0.
|
|
514
|
-
{ capabilities: { tools: {} }, instructions: INSTRUCTIONS }
|
|
590
|
+
{ name: 'miromiro', version: '0.7.0' },
|
|
591
|
+
{ capabilities: { tools: {}, prompts: {} }, instructions: INSTRUCTIONS }
|
|
515
592
|
)
|
|
516
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
|
+
|
|
517
610
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
518
611
|
tools: TOOLS.map(({ name, title, description, inputSchema, outputSchema }) => ({
|
|
519
612
|
name,
|