miromiro-mcp 0.7.3 → 0.8.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 +13 -7
- package/package.json +1 -1
- package/src/index.mjs +12 -11
package/README.md
CHANGED
|
@@ -32,8 +32,13 @@ https://miromiro.app/mcp
|
|
|
32
32
|
Paste that URL into any client that accepts a remote MCP server. On first use it opens a one-click MiroMiro sign-in that links a free account (300 credits/month, no card).
|
|
33
33
|
|
|
34
34
|
- **Claude (web or desktop):** Settings → Connectors → Add custom connector → paste the URL. Or use the [one-click install](https://miromiro.app/api/mcp).
|
|
35
|
-
- **ChatGPT:** Settings → Security and login → enable Developer mode, then
|
|
35
|
+
- **ChatGPT:** Settings → Security and login → enable Developer mode, then Settings → Plugins → New Plugin with the URL (authentication: OAuth).
|
|
36
36
|
- **Cursor (remote):** add `{ "mcpServers": { "miromiro": { "url": "https://miromiro.app/mcp" } } }` to `~/.cursor/mcp.json`.
|
|
37
|
+
- **Claude Code:** `claude mcp add --transport http miromiro https://miromiro.app/mcp`, then `/mcp` in a session to sign in.
|
|
38
|
+
- **Codex:** add `[mcp_servers.miromiro]` with `url = "https://miromiro.app/mcp"` to `~/.codex/config.toml`, then run `codex mcp login miromiro`.
|
|
39
|
+
- **Grok Bot:** ask the bot "Add a custom MCP server called miromiro at https://miromiro.app/mcp", then press Authorize.
|
|
40
|
+
- **Muse:** Settings → Connectors → Add custom connector → paste the URL.
|
|
41
|
+
- Step-by-step pages for each agent: https://miromiro.app/api/agents/chatgpt (swap the last part for `claude`, `claude-code`, `codex`, `cursor`, `grok-bot`, `muse` or `openai-dots`).
|
|
37
42
|
- **Raw API key instead of OAuth:** append `?access_key=mm_live_...` to the URL, or send it as an `Authorization: Bearer` header.
|
|
38
43
|
|
|
39
44
|
## Local server (npm)
|
|
@@ -66,15 +71,15 @@ claude mcp add miromiro -s user \
|
|
|
66
71
|
|
|
67
72
|
| Tool | What it returns | Credits |
|
|
68
73
|
|---|---|---|
|
|
69
|
-
| `extract_code` | A section of a live page as a paste-ready component (Tailwind, HTML+CSS, JSX or Vue) plus the tokens it uses - start here to rebuild real UI | 25 |
|
|
74
|
+
| `extract_code` | A section of a live page as a paste-ready component (Tailwind, HTML+CSS, JSX or Vue) or an importable Elementor template / Shopify section, plus the tokens it uses - start here to rebuild real UI | 25 |
|
|
70
75
|
| `extract_brand` | Named color roles (primary/accent/background/text), palette, fonts split by role, logo, social profiles, a 0-1 confidence score, and `signals` explaining where the colors came from (declared tokens vs inferred) | 15 |
|
|
71
76
|
| `extract_design_tokens` | Full token system with usage counts; `format: css` or `tailwind` for paste-ready output, `dtcg` for a W3C Design Tokens file (Style Dictionary / Figma variables) | 10 |
|
|
72
77
|
| `extract_fonts` | Every font file and family the site loads, with weights, formats, and file URLs | 5 |
|
|
73
|
-
| `extract_videos` | Video files, `og:video`, and player embeds (YouTube, Vimeo, Wistia, Loom), tagged by source | 1 |
|
|
74
|
-
| `extract_audio` | Audio files, `og:audio`, and podcast enclosures, tagged by source | 1 |
|
|
78
|
+
| `extract_videos` | Video files, `og:video`, and player embeds (YouTube, Vimeo, Wistia, Loom), tagged by source; a direct video-file URL is returned as the single result, and a YouTube/Vimeo/TikTok/Dailymotion/Loom video URL returns its player embed without a fetch | 1 |
|
|
79
|
+
| `extract_audio` | Audio files, `og:audio`, and podcast enclosures, tagged by source; a direct audio-file URL is returned as the single result | 1 |
|
|
75
80
|
| `extract_lottie` | Lottie animation JSON files declared in the page | 3 |
|
|
76
81
|
| `extract_svgs` | Inline SVG code (sanitized, paste-ready) plus external SVG file URLs | 1 |
|
|
77
|
-
| `extract_assets` | Every image URL on a page, tagged by source, with alt text | 1 |
|
|
82
|
+
| `extract_assets` | Every image URL on a page, tagged by source, with alt text; a direct image-file URL is returned as the single result | 1 |
|
|
78
83
|
|
|
79
84
|
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
85
|
|
|
@@ -104,7 +109,7 @@ Generated code is never trimmed. Results stop at roughly 20k tokens (25k for `ex
|
|
|
104
109
|
|
|
105
110
|
The REST API and this local npm server always return everything untrimmed, so `limit` and
|
|
106
111
|
`full_output` apply to the remote server only. Every tool on both servers accepts
|
|
107
|
-
`render: true` (paid API plans and credit packs
|
|
112
|
+
`render: true` (free accounts get 5 renders a month, then the call runs static and `usage.renderSkipped` says so; paid API plans and credit packs render without a monthly limit - see Notes), plus two more shared arguments:
|
|
108
113
|
|
|
109
114
|
- `fresh: true` - skip the 24-hour cache and re-extract the live page (charged normally); use when the site just changed
|
|
110
115
|
- `screenshot: true` (requires `render: true`) - the result gains a `screenshot` field: a public URL of a 1440x900 capture of the page, live for 7 days, at no extra credits - show it to the user to verify an extraction. Pass `'full'` instead of `true` for the entire page height.
|
|
@@ -131,7 +136,8 @@ The agent calls `extract_fonts` and returns every family with its font file URLs
|
|
|
131
136
|
|
|
132
137
|
## Notes
|
|
133
138
|
|
|
134
|
-
- The API reads static HTML and resolves the full CSS cascade by default, so a fully client-rendered app may return little for images, video, audio and Lottie (tokens, fonts, brand, and code come from CSS and are unaffected). Pass `render: true` on any remote-server tool to run the page in a real browser first so its JavaScript executes (+5 credits).
|
|
139
|
+
- The API reads static HTML and resolves the full CSS cascade by default, so a fully client-rendered app may return little for images, video, audio and Lottie (tokens, fonts, brand, and code come from CSS and are unaffected). Pass `render: true` on any remote-server tool to run the page in a real browser first so its JavaScript executes (+5 credits). Free accounts get 5 renders a month (`usage.freeRender` reports each one and how many are left); after that a `render: true` call runs as the static extraction and `usage.renderSkipped` says so - never a refusal. Paid API plans and credit packs render without a monthly limit.
|
|
140
|
+
- The same allowance also works without the flag: when the SITE refuses a free account's plain request (401, 403 or 406 - publishers commonly turn away datacenter IPs even for a public page), the server retries that call in a real browser automatically and free, a few times a month. The result then carries `usage.renderRescue` (a sentence to read to the user) and `usage.renderRescuesLeft`. So on a free account, do not treat a 401/403/406 as "this site cannot be extracted" until those are used up.
|
|
135
141
|
- Every result carries `usage` with the credits left this month. If the account holds a credit pack (one-off credits that never expire, bought at https://miromiro.app/api/pricing), `usage.packCredits` is the pack balance left; packs are drawn after the monthly credits, so a free account with a pack keeps working past its 300 credits.
|
|
136
142
|
- Respect the sites you extract from: check their terms before reusing assets commercially.
|
|
137
143
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "miromiro-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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
|
@@ -45,7 +45,7 @@ const NAMED_SECTIONS = ['hero', 'header', 'footer', 'pricing', 'features', 'test
|
|
|
45
45
|
// client-rendered React/Next/SPA sites that return little statically.
|
|
46
46
|
const RENDER_ARG = {
|
|
47
47
|
type: 'boolean',
|
|
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.
|
|
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. Never pass it as a first attempt: call static first and render only when the result is thin or the hint asks for it. Free accounts get 5 renders a month; after that a render=true call runs as the static extraction and usage.renderSkipped says so (never a refusal) - relay that sentence and do not pass render=true again that month. Paid API plans and credit packs render without a monthly limit. Free accounts also get rendering automatically, from the same allowance, when the SITE refuses the plain request (401/403/406): the server retries it in a browser at no charge and the result carries usage.renderRescue - read that sentence out to the user when it appears.'
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// Shared input arg: skip the 24h cache for this one call (charged normally).
|
|
@@ -82,14 +82,14 @@ const TOOLS = [
|
|
|
82
82
|
title: 'Extract section as code',
|
|
83
83
|
endpoint: 'code',
|
|
84
84
|
description:
|
|
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).",
|
|
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 - or as a file to import into a site builder: framework=elementor for an Elementor template (WordPress), framework=shopify for a Shopify theme section. 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).",
|
|
86
86
|
inputSchema: {
|
|
87
87
|
type: 'object',
|
|
88
88
|
properties: {
|
|
89
89
|
url: { type: 'string', description: 'The website URL, e.g. stripe.com' },
|
|
90
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 from its ids, classes, headings and, failing those, its content (prices, blockquotes, FAQ items), so it works without seeing the markup and on pages that label nothing. Omit for the hero. If the page has no such section you get the hero plus a `note` listing what is there." },
|
|
91
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.' },
|
|
92
|
-
framework: { type: 'string', enum: ['tailwind', 'html', 'jsx', 'vue'], description:
|
|
92
|
+
framework: { type: 'string', enum: ['tailwind', 'html', 'jsx', 'vue', 'elementor', 'shopify'], description: "The code to generate (default tailwind). 'elementor' returns an importable Elementor template: tell the user to save `code` as a .json file and import it in WordPress under Templates > Saved Templates > Import. 'shopify' returns a theme section: save `code` as sections/<name>.liquid in the theme, then add it from the theme editor. Use the theme code editor's sections folder, not a Custom Liquid block (that block is capped at 50 KB and cannot hold the schema). Both arrive as one styled HTML block (edited as code, not as native widgets), with the section's responsive and hover styles included and its CSS scoped so it cannot clash with the theme. Give the user the file contents complete and unmodified." },
|
|
93
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.' },
|
|
94
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." },
|
|
95
95
|
render: RENDER_ARG, fresh: FRESH_ARG, screenshot: SCREENSHOT_ARG
|
|
@@ -118,9 +118,9 @@ const TOOLS = [
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
},
|
|
121
|
-
framework: { type: 'string', description: 'tailwind | html | jsx | vue' },
|
|
122
|
-
code: { type: 'string', description: 'The generated, paste-ready component code. For jsx/vue, repeated structures (tiers, cards, list items) are collapsed into a data array rendered with .map()/v-for' },
|
|
123
|
-
css: { type: 'string', description: 'Companion CSS: always set for framework=html; for
|
|
121
|
+
framework: { type: 'string', description: 'tailwind | html | jsx | vue | elementor | shopify' },
|
|
122
|
+
code: { type: 'string', description: 'The generated, paste-ready component code. For jsx/vue, repeated structures (tiers, cards, list items) are collapsed into a data array rendered with .map()/v-for. For elementor this is the complete template JSON (a .json file); for shopify the complete section file (a .liquid file) - hand either over whole' },
|
|
123
|
+
css: { type: 'string', description: 'Companion CSS: always set for framework=html; for tailwind/jsx/vue it carries the @keyframes the section uses (add to the global stylesheet); null for elementor/shopify, whose file already contains its CSS' },
|
|
124
124
|
tokens: { type: 'object', description: 'Design tokens the extracted section actually uses' },
|
|
125
125
|
theme: { type: 'string', description: "light | dark: which one the code you just received actually is. Measured from the page, never echoed from your request - so if you asked for a theme the site cannot serve, this reports what you really got. Tell the user which theme they got when it may not match what they see on the site" },
|
|
126
126
|
assets: { description: 'Images/fonts the section references' },
|
|
@@ -235,7 +235,7 @@ const TOOLS = [
|
|
|
235
235
|
title: 'Extract images',
|
|
236
236
|
endpoint: 'images',
|
|
237
237
|
description:
|
|
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).",
|
|
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). A direct image-file URL comes back as the single result (source file). 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).",
|
|
239
239
|
inputSchema: {
|
|
240
240
|
type: 'object',
|
|
241
241
|
properties: { url: { type: 'string', description: 'The website URL' }, render: RENDER_ARG, fresh: FRESH_ARG, screenshot: SCREENSHOT_ARG },
|
|
@@ -399,7 +399,7 @@ const TOOLS = [
|
|
|
399
399
|
title: 'Extract videos',
|
|
400
400
|
endpoint: 'videos',
|
|
401
401
|
description:
|
|
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).',
|
|
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; a direct video-file URL comes back as the single result (source file). A YouTube, Vimeo, TikTok, Dailymotion or Loom video URL is answered from the URL alone with its player embed (source platform, no fetch, never pass render for these) - tell the user the file itself cannot be downloaded through MiroMiro. Most other players mount with JavaScript: if the result is thin or empty, call again with render=true (+5 credits).',
|
|
403
403
|
inputSchema: {
|
|
404
404
|
type: 'object',
|
|
405
405
|
properties: { url: { type: 'string', description: 'The website URL' }, render: RENDER_ARG, fresh: FRESH_ARG, screenshot: SCREENSHOT_ARG },
|
|
@@ -435,7 +435,7 @@ const TOOLS = [
|
|
|
435
435
|
title: 'Extract audio',
|
|
436
436
|
endpoint: 'audio',
|
|
437
437
|
description:
|
|
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).',
|
|
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; a direct audio-file URL comes back as the single result (source file). If a JavaScript player mounts the audio at runtime, call again with render=true (+5 credits).',
|
|
439
439
|
inputSchema: {
|
|
440
440
|
type: 'object',
|
|
441
441
|
properties: { url: { type: 'string', description: 'The website URL' }, render: RENDER_ARG, fresh: FRESH_ARG, screenshot: SCREENSHOT_ARG },
|
|
@@ -531,7 +531,7 @@ const PROMPTS = [
|
|
|
531
531
|
arguments: [
|
|
532
532
|
PROMPT_URL_ARG,
|
|
533
533
|
{ name: 'section', description: `Which part: ${NAMED_SECTIONS.join(', ')} (default hero)` },
|
|
534
|
-
{ name: 'framework', description: 'tailwind (default), html, jsx or
|
|
534
|
+
{ name: 'framework', description: 'tailwind (default), html, jsx, vue, elementor or shopify' }
|
|
535
535
|
],
|
|
536
536
|
render: ({ url, section, framework }) =>
|
|
537
537
|
`Rebuild the ${section || 'hero'} section of ${url} as ${framework || 'tailwind'} code I can paste into my project. ` +
|
|
@@ -580,6 +580,7 @@ const INSTRUCTIONS =
|
|
|
580
580
|
'`hint`, or a 403 target_blocked comes back, retry the SAME url with render=true (+5 credits) before telling the ' +
|
|
581
581
|
'user a site cannot be extracted. Theme: a site with a prefers-color-scheme dark variant returns its light branch ' +
|
|
582
582
|
'by default; pass theme=dark to extract_code for the dark version and report the `theme` field you actually got. ' +
|
|
583
|
+
'For WordPress/Elementor or Shopify users, extract_code framework=elementor|shopify returns an importable template (.json) or theme section (.liquid) instead of component code. ' +
|
|
583
584
|
'extract_design_tokens returns values and counts, not layout - not a substitute for extract_code on a rebuild; ' +
|
|
584
585
|
'format=dtcg gives a W3C Design Tokens file. Explain results, do not dump them: extract_brand\'s ' +
|
|
585
586
|
'signals.explanation says whether the colors were declared by the site or inferred (primarySource "usage" = a ' +
|
|
@@ -587,7 +588,7 @@ const INSTRUCTIONS =
|
|
|
587
588
|
'fresh=true bypasses the 24h cache. Every result carries `usage` (credits left; `packCredits` = never-expiring pack balance when the account holds one).'
|
|
588
589
|
|
|
589
590
|
const server = new Server(
|
|
590
|
-
{ name: 'miromiro', version: '0.
|
|
591
|
+
{ name: 'miromiro', version: '0.8.0' },
|
|
591
592
|
{ capabilities: { tools: {}, prompts: {} }, instructions: INSTRUCTIONS }
|
|
592
593
|
)
|
|
593
594
|
|