miromiro-mcp 0.1.0 → 0.2.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 +1 -1
  2. package/package.json +32 -32
  3. package/src/index.mjs +199 -176
package/README.md CHANGED
@@ -4,7 +4,7 @@ MCP server for the [MiroMiro](https://miromiro.app/api) design-extraction API. G
4
4
 
5
5
  ## Setup
6
6
 
7
- 1. Get a free API key (100 credits/month, no card) at **https://miromiro.app/app/developers**.
7
+ 1. Get a free API key (100 credits/month, no card) at **https://miromiro.app/api/dashboard**.
8
8
  2. Add the server to your agent's MCP config:
9
9
 
10
10
  **Cursor** (`~/.cursor/mcp.json`) / **Windsurf** (Settings → MCP):
package/package.json CHANGED
@@ -1,32 +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
- }
1
+ {
2
+ "name": "miromiro-mcp",
3
+ "version": "0.2.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 CHANGED
@@ -1,176 +1,199 @@
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')
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/api/dashboard).
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
+ // Every tool is a read against the public web: no writes, retry-safe (cached
29
+ // repeats), open-world. Clients use readOnlyHint to label tools as safe.
30
+ const TOOL_ANNOTATIONS = {
31
+ readOnlyHint: true,
32
+ destructiveHint: false,
33
+ idempotentHint: true,
34
+ openWorldHint: true
35
+ }
36
+
37
+ const TOOLS = [
38
+ {
39
+ name: 'extract_code',
40
+ title: 'Extract section as code',
41
+ endpoint: 'code',
42
+ description:
43
+ "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.",
44
+ inputSchema: {
45
+ type: 'object',
46
+ properties: {
47
+ url: { type: 'string', description: 'The website URL, e.g. stripe.com' },
48
+ selector: { type: 'string', description: 'CSS selector for the section to extract, e.g. "#hero" or ".pricing-card". Omit to auto-detect the hero.' },
49
+ framework: { type: 'string', enum: ['tailwind', 'html', 'jsx', 'vue'], description: 'The code to generate (default tailwind).' },
50
+ name: { type: 'string', description: 'Component name for the jsx/vue targets (default ExtractedSection).' }
51
+ },
52
+ required: ['url']
53
+ }
54
+ },
55
+ {
56
+ name: 'extract_brand',
57
+ title: 'Extract brand identity',
58
+ endpoint: 'brand',
59
+ description:
60
+ "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.",
61
+ inputSchema: {
62
+ type: 'object',
63
+ properties: { url: { type: 'string', description: 'The website URL, e.g. stripe.com' } },
64
+ required: ['url']
65
+ }
66
+ },
67
+ {
68
+ name: 'extract_design_tokens',
69
+ title: 'Extract design tokens',
70
+ endpoint: 'extract',
71
+ description:
72
+ "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.",
73
+ inputSchema: {
74
+ type: 'object',
75
+ properties: {
76
+ url: { type: 'string', description: 'The website URL' },
77
+ format: { type: 'string', enum: ['json', 'css', 'tailwind'], description: 'Output shape (default json)' }
78
+ },
79
+ required: ['url']
80
+ }
81
+ },
82
+ {
83
+ name: 'extract_assets',
84
+ title: 'Extract images',
85
+ endpoint: 'images',
86
+ description:
87
+ "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.",
88
+ inputSchema: {
89
+ type: 'object',
90
+ properties: { url: { type: 'string', description: 'The website URL' } },
91
+ required: ['url']
92
+ }
93
+ },
94
+ {
95
+ name: 'extract_svgs',
96
+ title: 'Extract SVGs',
97
+ endpoint: 'svg',
98
+ description:
99
+ "Extract inline SVGs (as paste-ready code) and SVG file references from a page. Use to grab icons and vector logos.",
100
+ inputSchema: {
101
+ type: 'object',
102
+ properties: { url: { type: 'string', description: 'The website URL' } },
103
+ required: ['url']
104
+ }
105
+ },
106
+ {
107
+ name: 'extract_fonts',
108
+ title: 'Extract fonts',
109
+ endpoint: 'fonts',
110
+ description:
111
+ "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.",
112
+ inputSchema: {
113
+ type: 'object',
114
+ properties: {
115
+ url: { type: 'string', description: 'The website URL' },
116
+ fields: { type: 'string', description: "Optional comma-separated fields to return (e.g. 'families')" }
117
+ },
118
+ required: ['url']
119
+ }
120
+ },
121
+ {
122
+ name: 'extract_lottie',
123
+ title: 'Extract Lottie animations',
124
+ endpoint: 'lottie',
125
+ description:
126
+ "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.",
127
+ inputSchema: {
128
+ type: 'object',
129
+ properties: { url: { type: 'string', description: 'The website URL' } },
130
+ required: ['url']
131
+ }
132
+ }
133
+ ]
134
+
135
+ // Query params a tool may forward to the API, beyond `url`. Anything else in the
136
+ // tool arguments is ignored, so a hallucinated param can't reach the endpoint.
137
+ const FORWARDED_PARAMS = ['format', 'selector', 'framework', 'name', 'fields']
138
+
139
+ async function callApi(endpoint, args) {
140
+ const q = new URLSearchParams({ url: args.url })
141
+ for (const p of FORWARDED_PARAMS) {
142
+ if (args[p] !== undefined && args[p] !== null && args[p] !== '') {
143
+ q.set(p, String(args[p]))
144
+ }
145
+ }
146
+ const res = await fetch(`${API_BASE}/${endpoint}?${q}`, {
147
+ headers: { Authorization: `Bearer ${API_KEY}` }
148
+ })
149
+ const isJson = (res.headers.get('content-type') || '').includes('application/json')
150
+ const body = isJson ? await res.json() : await res.text()
151
+ if (!res.ok) {
152
+ const msg = isJson ? (body.statusMessage || body.message || JSON.stringify(body)) : body
153
+ throw new Error(`MiroMiro API ${res.status}: ${msg}`)
154
+ }
155
+ // Compact JSON: pretty-printing spends ~30% more model context on whitespace.
156
+ return isJson ? JSON.stringify(body) : body
157
+ }
158
+
159
+ const server = new Server(
160
+ { name: 'miromiro', version: '0.2.0' },
161
+ { capabilities: { tools: {} } }
162
+ )
163
+
164
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
165
+ tools: TOOLS.map(({ name, title, description, inputSchema }) => ({
166
+ name,
167
+ title,
168
+ description,
169
+ inputSchema,
170
+ annotations: { title, ...TOOL_ANNOTATIONS }
171
+ }))
172
+ }))
173
+
174
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
175
+ if (!API_KEY) {
176
+ return {
177
+ isError: true,
178
+ content: [{ type: 'text', text: 'MIROMIRO_API_KEY is not set. Get a free key at https://miromiro.app/api/dashboard and set it in your MCP config env.' }]
179
+ }
180
+ }
181
+ const tool = TOOLS.find(t => t.name === req.params.name)
182
+ if (!tool) {
183
+ return { isError: true, content: [{ type: 'text', text: `Unknown tool: ${req.params.name}` }] }
184
+ }
185
+ const args = req.params.arguments || {}
186
+ if (!args.url) {
187
+ return { isError: true, content: [{ type: 'text', text: 'The "url" argument is required.' }] }
188
+ }
189
+ try {
190
+ const out = await callApi(tool.endpoint, args)
191
+ return { content: [{ type: 'text', text: out }] }
192
+ } catch (err) {
193
+ return { isError: true, content: [{ type: 'text', text: String(err.message || err) }] }
194
+ }
195
+ })
196
+
197
+ const transport = new StdioServerTransport()
198
+ await server.connect(transport)
199
+ console.error('miromiro-mcp running on stdio')