miromiro-mcp 0.1.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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +56 -0
  3. package/package.json +32 -0
  4. package/src/index.mjs +176 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MiroMiro (Soraia Barroso)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # miromiro-mcp
2
+
3
+ MCP server for the [MiroMiro](https://miromiro.app/api) design-extraction API. Give your coding agent (Cursor, Claude Code, Windsurf) the real design of any website — code, tokens, assets, SVGs, fonts, and Lottie — so it builds UI in that style instead of guessing.
4
+
5
+ ## Setup
6
+
7
+ 1. Get a free API key (100 credits/month, no card) at **https://miromiro.app/app/developers**.
8
+ 2. Add the server to your agent's MCP config:
9
+
10
+ **Cursor** (`~/.cursor/mcp.json`) / **Windsurf** (Settings → MCP):
11
+
12
+ ```json
13
+ {
14
+ "mcpServers": {
15
+ "miromiro": {
16
+ "command": "npx",
17
+ "args": ["-y", "miromiro-mcp"],
18
+ "env": { "MIROMIRO_API_KEY": "mm_live_your_key" }
19
+ }
20
+ }
21
+ }
22
+ ```
23
+
24
+ **Claude Code** (run in your terminal, not inside a `claude` session):
25
+
26
+ ```bash
27
+ claude mcp add miromiro -s user \
28
+ --env MIROMIRO_API_KEY=mm_live_your_key \
29
+ -- npx -y miromiro-mcp
30
+ ```
31
+
32
+ ## Tools
33
+
34
+ | Tool | What it returns | Credits |
35
+ |---|---|---|
36
+ | `extract_code` | A section of a live page as a paste-ready component (Tailwind, HTML+CSS, JSX or Vue) + the tokens it uses — start here to rebuild real UI | 25 |
37
+ | `extract_brand` | Named color roles (primary/accent/background/text), palette, fonts, logo | 15 |
38
+ | `extract_design_tokens` | Full token system; `format: css` or `tailwind` for paste-ready output | 10 |
39
+ | `extract_fonts` | Font files & families the site loads | 5 |
40
+ | `extract_lottie` | Lottie animations declared in the page | 3 |
41
+ | `extract_svgs` | Inline SVG code + SVG files | 1 |
42
+ | `extract_assets` | Every image URL on a page | 1 |
43
+
44
+ ## Example
45
+
46
+ > "Rebuild stripe.com's pricing section in React"
47
+
48
+ The agent calls `extract_code` with the URL → gets the section's real markup, colors, fonts and spacing → adapts actual values instead of guessing from a screenshot.
49
+
50
+ > "Build a pricing section in the style of linear.app"
51
+
52
+ The agent calls `extract_brand` → gets Linear's real colors and fonts → builds the section matching them.
53
+
54
+ ## Credits
55
+
56
+ Weighted per endpoint (see table above). Cached and failed calls are free. Full reference: [docs](https://miromiro.app/api/docs) · [pricing](https://miromiro.app/api/pricing).
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "miromiro-mcp",
3
+ "version": "0.1.0",
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
+ "type": "module",
6
+ "author": "MiroMiro (https://miromiro.app)",
7
+ "homepage": "https://miromiro.app/api",
8
+ "bugs": { "url": "https://miromiro.app/api/docs/errors" },
9
+ "bin": {
10
+ "miromiro-mcp": "src/index.mjs"
11
+ },
12
+ "files": [
13
+ "src"
14
+ ],
15
+ "keywords": [
16
+ "mcp",
17
+ "model-context-protocol",
18
+ "design-tokens",
19
+ "design-to-code",
20
+ "miromiro",
21
+ "cursor",
22
+ "claude-code",
23
+ "windsurf"
24
+ ],
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "dependencies": {
29
+ "@modelcontextprotocol/sdk": "^1.0.0"
30
+ },
31
+ "license": "MIT"
32
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MiroMiro MCP server (playbook §3).
4
+ *
5
+ * Exposes the MiroMiro design-extraction API as MCP tools so a coding agent
6
+ * (Cursor / Claude Code / Windsurf) can pull the real design of any URL and
7
+ * build UI in that style — instead of guessing at colors and fonts.
8
+ *
9
+ * Auth: set MIROMIRO_API_KEY in the environment (get one free, no card, at
10
+ * https://miromiro.app/app/developers).
11
+ *
12
+ * Run: MIROMIRO_API_KEY=mm_live_… npx miromiro-mcp
13
+ * Or wire it into your agent's MCP config (see README).
14
+ */
15
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
16
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
17
+ import {
18
+ CallToolRequestSchema,
19
+ ListToolsRequestSchema
20
+ } from '@modelcontextprotocol/sdk/types.js'
21
+
22
+ const API_BASE = process.env.MIROMIRO_API_BASE || 'https://miromiro.app/api/v1'
23
+ const API_KEY = process.env.MIROMIRO_API_KEY
24
+
25
+ // Tool → endpoint map. Each tool takes a single `url` (design_tokens also takes
26
+ // an optional `format`). Descriptions are written for the agent, so it knows
27
+ // WHEN to reach for each one.
28
+ const TOOLS = [
29
+ {
30
+ name: 'extract_code',
31
+ endpoint: 'code',
32
+ description:
33
+ "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. 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 and does not execute JavaScript, so a fully client-rendered app may return little.",
34
+ inputSchema: {
35
+ type: 'object',
36
+ properties: {
37
+ url: { type: 'string', description: 'The website URL, e.g. stripe.com' },
38
+ selector: { type: 'string', description: 'CSS selector for the section to extract, e.g. "#hero" or ".pricing-card". Omit to auto-detect the hero.' },
39
+ framework: { type: 'string', enum: ['tailwind', 'html', 'jsx', 'vue'], description: 'The code to generate (default tailwind).' },
40
+ name: { type: 'string', description: 'Component name for the jsx/vue targets (default ExtractedSection).' }
41
+ },
42
+ required: ['url']
43
+ }
44
+ },
45
+ {
46
+ name: 'extract_brand',
47
+ endpoint: 'brand',
48
+ description:
49
+ "Get a website's curated brand identity: named color roles (primary, accent, background, text), the brand palette, primary fonts, logo, and social image. 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.",
50
+ inputSchema: {
51
+ type: 'object',
52
+ properties: { url: { type: 'string', description: 'The website URL, e.g. stripe.com' } },
53
+ required: ['url']
54
+ }
55
+ },
56
+ {
57
+ name: 'extract_design_tokens',
58
+ endpoint: 'extract',
59
+ description:
60
+ "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 or format='tailwind' for a tailwind.config — paste-ready. Use when you need the complete token set, not just the brand summary.",
61
+ inputSchema: {
62
+ type: 'object',
63
+ properties: {
64
+ url: { type: 'string', description: 'The website URL' },
65
+ format: { type: 'string', enum: ['json', 'css', 'tailwind'], description: 'Output shape (default json)' }
66
+ },
67
+ required: ['url']
68
+ }
69
+ },
70
+ {
71
+ name: 'extract_assets',
72
+ endpoint: 'images',
73
+ description:
74
+ "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.",
75
+ inputSchema: {
76
+ type: 'object',
77
+ properties: { url: { type: 'string', description: 'The website URL' } },
78
+ required: ['url']
79
+ }
80
+ },
81
+ {
82
+ name: 'extract_svgs',
83
+ endpoint: 'svg',
84
+ description:
85
+ "Extract inline SVGs (as paste-ready code) and SVG file references from a page. Use to grab icons and vector logos.",
86
+ inputSchema: {
87
+ type: 'object',
88
+ properties: { url: { type: 'string', description: 'The website URL' } },
89
+ required: ['url']
90
+ }
91
+ },
92
+ {
93
+ name: 'extract_fonts',
94
+ endpoint: 'fonts',
95
+ description:
96
+ "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.",
97
+ inputSchema: {
98
+ type: 'object',
99
+ properties: {
100
+ url: { type: 'string', description: 'The website URL' },
101
+ fields: { type: 'string', description: "Optional comma-separated fields to return (e.g. 'families')" }
102
+ },
103
+ required: ['url']
104
+ }
105
+ },
106
+ {
107
+ name: 'extract_lottie',
108
+ endpoint: 'lottie',
109
+ description:
110
+ "Find Lottie/animation JSON files declared on a page. Best-effort from static HTML (runtime-injected animations need the MiroMiro browser extension). Use to recover animations.",
111
+ inputSchema: {
112
+ type: 'object',
113
+ properties: { url: { type: 'string', description: 'The website URL' } },
114
+ required: ['url']
115
+ }
116
+ }
117
+ ]
118
+
119
+ // Query params a tool may forward to the API, beyond `url`. Anything else in the
120
+ // tool arguments is ignored, so a hallucinated param can't reach the endpoint.
121
+ const FORWARDED_PARAMS = ['format', 'selector', 'framework', 'name', 'fields']
122
+
123
+ async function callApi(endpoint, args) {
124
+ const q = new URLSearchParams({ url: args.url })
125
+ for (const p of FORWARDED_PARAMS) {
126
+ if (args[p] !== undefined && args[p] !== null && args[p] !== '') {
127
+ q.set(p, String(args[p]))
128
+ }
129
+ }
130
+ const res = await fetch(`${API_BASE}/${endpoint}?${q}`, {
131
+ headers: { Authorization: `Bearer ${API_KEY}` }
132
+ })
133
+ const isJson = (res.headers.get('content-type') || '').includes('application/json')
134
+ const body = isJson ? await res.json() : await res.text()
135
+ if (!res.ok) {
136
+ const msg = isJson ? (body.statusMessage || body.message || JSON.stringify(body)) : body
137
+ throw new Error(`MiroMiro API ${res.status}: ${msg}`)
138
+ }
139
+ return isJson ? JSON.stringify(body, null, 2) : body
140
+ }
141
+
142
+ const server = new Server(
143
+ { name: 'miromiro', version: '0.1.0' },
144
+ { capabilities: { tools: {} } }
145
+ )
146
+
147
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
148
+ tools: TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema }))
149
+ }))
150
+
151
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
152
+ if (!API_KEY) {
153
+ return {
154
+ isError: true,
155
+ content: [{ type: 'text', text: 'MIROMIRO_API_KEY is not set. Get a free key at https://miromiro.app/app/developers and set it in your MCP config env.' }]
156
+ }
157
+ }
158
+ const tool = TOOLS.find(t => t.name === req.params.name)
159
+ if (!tool) {
160
+ return { isError: true, content: [{ type: 'text', text: `Unknown tool: ${req.params.name}` }] }
161
+ }
162
+ const args = req.params.arguments || {}
163
+ if (!args.url) {
164
+ return { isError: true, content: [{ type: 'text', text: 'The "url" argument is required.' }] }
165
+ }
166
+ try {
167
+ const out = await callApi(tool.endpoint, args)
168
+ return { content: [{ type: 'text', text: out }] }
169
+ } catch (err) {
170
+ return { isError: true, content: [{ type: 'text', text: String(err.message || err) }] }
171
+ }
172
+ })
173
+
174
+ const transport = new StdioServerTransport()
175
+ await server.connect(transport)
176
+ console.error('miromiro-mcp running on stdio')