miromiro-mcp 0.5.0 → 0.6.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 +1 -1
- package/package.json +1 -1
- package/src/index.mjs +320 -7
package/README.md
CHANGED
|
@@ -67,7 +67,7 @@ claude mcp add miromiro -s user \
|
|
|
67
67
|
| Tool | What it returns | Credits |
|
|
68
68
|
|---|---|---|
|
|
69
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 |
|
|
70
|
-
| `extract_brand` | Named color roles (primary/accent/background/text), palette, fonts split by role, logo, social profiles,
|
|
70
|
+
| `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
71
|
| `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
72
|
| `extract_fonts` | Every font file and family the site loads, with weights, formats, and file URLs | 5 |
|
|
73
73
|
| `extract_videos` | Video files, `og:video`, and player embeds (YouTube, Vimeo, Wistia, Loom), tagged by source | 1 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "miromiro-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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
|
@@ -54,6 +54,22 @@ const SCREENSHOT_ARG = {
|
|
|
54
54
|
description: "Requires render=true. Adds a screenshot field to the result: a public URL of a capture of the page (live 7 days), taken by the same browser that rendered it. Pass true for the 1440x900 viewport or 'full' for the entire page height. No extra credits beyond the render surcharge. Useful for showing the user what the extracted site looks like."
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// Shared fragments for the output schemas below, mirrored from the remote
|
|
58
|
+
// server (server/utils/mcpRemote.ts). Every schema keeps additionalProperties
|
|
59
|
+
// true and requires only `url`: the /v1 responses can carry extras (usage,
|
|
60
|
+
// note, truncation_note) and a strict schema that drifts from the
|
|
61
|
+
// implementation would make clients reject valid results.
|
|
62
|
+
const OUT_USAGE = { type: 'object', description: 'Credit usage for the authed key (spent, remaining, monthly limit).' }
|
|
63
|
+
const OUT_TRUNCATION = { type: 'string', description: 'Present when the result was trimmed - says what was dropped and how to get more.' }
|
|
64
|
+
const countedList = (valueKey, desc) => ({
|
|
65
|
+
type: 'array',
|
|
66
|
+
description: desc,
|
|
67
|
+
items: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: { [valueKey]: { type: 'string' }, count: { type: 'number', description: 'How many times it is used on the page' } }
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
57
73
|
const TOOLS = [
|
|
58
74
|
{
|
|
59
75
|
name: 'extract_code',
|
|
@@ -72,6 +88,40 @@ const TOOLS = [
|
|
|
72
88
|
render: RENDER_ARG, fresh: FRESH_ARG, screenshot: SCREENSHOT_ARG
|
|
73
89
|
},
|
|
74
90
|
required: ['url']
|
|
91
|
+
},
|
|
92
|
+
outputSchema: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
properties: {
|
|
95
|
+
url: { type: 'string' },
|
|
96
|
+
selector: { type: 'string', description: 'The CSS selector that was extracted' },
|
|
97
|
+
autoDetected: { type: 'boolean', description: 'True when the hero was auto-detected rather than passed in' },
|
|
98
|
+
sections: {
|
|
99
|
+
type: 'array',
|
|
100
|
+
description: 'Every extractable section on the page - call again with one of these selectors for a specific part',
|
|
101
|
+
items: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: {
|
|
104
|
+
selector: { type: 'string' },
|
|
105
|
+
label: { type: 'string' },
|
|
106
|
+
elements: { type: 'number' },
|
|
107
|
+
hasHeading: { type: 'boolean' },
|
|
108
|
+
preview: { type: 'string' }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
framework: { type: 'string', description: 'tailwind | html | jsx | vue' },
|
|
113
|
+
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' },
|
|
114
|
+
css: { type: 'string', description: 'Companion CSS: always set for framework=html; for other frameworks it carries the @keyframes the section uses (add to the global stylesheet)' },
|
|
115
|
+
tokens: { type: 'object', description: 'Design tokens the extracted section actually uses' },
|
|
116
|
+
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" },
|
|
117
|
+
assets: { description: 'Images/fonts the section references' },
|
|
118
|
+
summary: { description: 'What was extracted, at a glance. styledCoverage (0-100) is the quality signal: the % of elements the cascade found styles for' },
|
|
119
|
+
hint: { type: 'string', description: 'Only present when the extraction looks degraded (client-rendered page, runtime-injected styles) - says what to do, usually retry with render=true' },
|
|
120
|
+
usage: OUT_USAGE,
|
|
121
|
+
truncation_note: OUT_TRUNCATION
|
|
122
|
+
},
|
|
123
|
+
required: ['url'],
|
|
124
|
+
additionalProperties: true
|
|
75
125
|
}
|
|
76
126
|
},
|
|
77
127
|
{
|
|
@@ -79,11 +129,56 @@ const TOOLS = [
|
|
|
79
129
|
title: 'Extract brand identity',
|
|
80
130
|
endpoint: 'brand',
|
|
81
131
|
description:
|
|
82
|
-
"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.",
|
|
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.",
|
|
83
133
|
inputSchema: {
|
|
84
134
|
type: 'object',
|
|
85
135
|
properties: { url: { type: 'string', description: 'The website URL, e.g. stripe.com' }, render: RENDER_ARG, fresh: FRESH_ARG, screenshot: SCREENSHOT_ARG },
|
|
86
136
|
required: ['url']
|
|
137
|
+
},
|
|
138
|
+
outputSchema: {
|
|
139
|
+
type: 'object',
|
|
140
|
+
properties: {
|
|
141
|
+
url: { type: 'string' },
|
|
142
|
+
title: { type: ['string', 'null'], description: 'Page/og title - the brand name' },
|
|
143
|
+
description: { type: ['string', 'null'] },
|
|
144
|
+
siteName: { type: ['string', 'null'] },
|
|
145
|
+
themeColor: { type: ['string', 'null'], description: "The site's own declared theme-color" },
|
|
146
|
+
theme: { type: 'string', description: 'dark | light' },
|
|
147
|
+
colors: { type: 'object', description: 'Role colors as hex: primary, accent, background, text' },
|
|
148
|
+
colorNames: { type: 'object', description: 'Human-readable names for the role colors ("Royal Blue")' },
|
|
149
|
+
palette: {
|
|
150
|
+
type: 'array',
|
|
151
|
+
description: 'The wider brand palette',
|
|
152
|
+
items: { type: 'object', properties: { color: { type: 'string' }, count: { type: 'number' }, name: { type: ['string', 'null'] } } }
|
|
153
|
+
},
|
|
154
|
+
fonts: { type: 'array', items: { type: 'string' } },
|
|
155
|
+
typography: { type: 'object', description: 'Fonts split by role: primary, mono' },
|
|
156
|
+
logo: { type: ['string', 'null'], description: 'Best single logo URL' },
|
|
157
|
+
logos: { type: 'array', description: 'All icon/logo candidates, tagged by source' },
|
|
158
|
+
ogImage: { type: ['string', 'null'] },
|
|
159
|
+
socials: {
|
|
160
|
+
type: 'array',
|
|
161
|
+
description: 'Social profiles found on the page',
|
|
162
|
+
items: { type: 'object', properties: { platform: { type: 'string' }, url: { type: 'string' }, handle: { type: ['string', 'null'] } } }
|
|
163
|
+
},
|
|
164
|
+
confidence: { type: 'number', description: '0-1: how much the site declared vs how much was inferred' },
|
|
165
|
+
signals: {
|
|
166
|
+
type: 'object',
|
|
167
|
+
description: "WHY the result looks the way it does - the provenance behind `confidence`. Read `signals.explanation` and tell the user what it says instead of quoting the confidence number, which means nothing to them on its own. `primarySource` is the key field: 'css-var' or 'theme-color' means the site declared these colors and they are its real tokens; 'logo' means they were sampled from its logo; 'usage' means nothing was declared and the colors are a frequency-based GUESS - say so rather than presenting them as the brand's official palette.",
|
|
168
|
+
properties: {
|
|
169
|
+
primarySource: { type: ['string', 'null'], description: "css-var | theme-color | logo | usage | null - where colors.primary came from, strongest first. 'usage' is inferred, not declared." },
|
|
170
|
+
declaredBackground: { type: 'boolean', description: 'The background role came from a declared CSS variable' },
|
|
171
|
+
declaredText: { type: 'boolean', description: 'The text role came from a declared CSS variable' },
|
|
172
|
+
declaredLogo: { type: 'boolean', description: 'A real logo was found, not just an og:image fallback' },
|
|
173
|
+
fontsFound: { type: 'number', description: 'How many font families resolved from the CSS cascade' },
|
|
174
|
+
explanation: { type: 'string', description: 'One ready-to-relay sentence describing how much was declared vs inferred. Repeat this to the user when the extraction is uncertain.' }
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
usage: OUT_USAGE,
|
|
178
|
+
truncation_note: OUT_TRUNCATION
|
|
179
|
+
},
|
|
180
|
+
required: ['url'],
|
|
181
|
+
additionalProperties: true
|
|
87
182
|
}
|
|
88
183
|
},
|
|
89
184
|
{
|
|
@@ -91,7 +186,7 @@ const TOOLS = [
|
|
|
91
186
|
title: 'Extract design tokens',
|
|
92
187
|
endpoint: 'extract',
|
|
93
188
|
description:
|
|
94
|
-
"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.",
|
|
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.",
|
|
95
190
|
inputSchema: {
|
|
96
191
|
type: 'object',
|
|
97
192
|
properties: {
|
|
@@ -100,6 +195,30 @@ const TOOLS = [
|
|
|
100
195
|
render: RENDER_ARG, fresh: FRESH_ARG, screenshot: SCREENSHOT_ARG
|
|
101
196
|
},
|
|
102
197
|
required: ['url']
|
|
198
|
+
},
|
|
199
|
+
outputSchema: {
|
|
200
|
+
type: 'object',
|
|
201
|
+
description: "format='json' returns the token lists below; format='css'|'tailwind' returns { output } - the paste-ready code block - instead; format='dtcg' returns a standalone W3C Design Tokens document (groups keyed by $type).",
|
|
202
|
+
properties: {
|
|
203
|
+
url: { type: 'string' },
|
|
204
|
+
colors: countedList('color', 'Every color with usage count'),
|
|
205
|
+
fontFamilies: countedList('family', 'Font families with usage counts'),
|
|
206
|
+
fontSizes: countedList('size', 'Font sizes with usage counts'),
|
|
207
|
+
fontWeights: countedList('value', 'Font weights'),
|
|
208
|
+
spacing: countedList('value', 'The spacing scale from margin/padding/gap'),
|
|
209
|
+
borderRadiuses: countedList('value', 'Border radii'),
|
|
210
|
+
boxShadows: countedList('value', 'Box shadows'),
|
|
211
|
+
lineHeights: countedList('value', 'Line heights'),
|
|
212
|
+
gradients: { type: 'array', description: 'Gradients with type (linear/radial/conic) and count' },
|
|
213
|
+
breakpoints: countedList('value', 'Responsive breakpoints from media queries'),
|
|
214
|
+
colorSchemes: { type: 'array', description: 'The colour schemes this page declares: ["light"] or ["light","dark"]. When both, the counts above are the union of the two themes and neither is labelled - pick colours by role, not by frequency, and use extract_code with theme=dark when you need one theme on its own' },
|
|
215
|
+
summary: { type: 'object', description: 'Count per token group' },
|
|
216
|
+
output: { type: 'string', description: "The paste-ready :root{} or tailwind.config block when format='css'|'tailwind'" },
|
|
217
|
+
usage: OUT_USAGE,
|
|
218
|
+
truncation_note: OUT_TRUNCATION
|
|
219
|
+
},
|
|
220
|
+
required: [],
|
|
221
|
+
additionalProperties: true
|
|
103
222
|
}
|
|
104
223
|
},
|
|
105
224
|
{
|
|
@@ -112,6 +231,29 @@ const TOOLS = [
|
|
|
112
231
|
type: 'object',
|
|
113
232
|
properties: { url: { type: 'string', description: 'The website URL' }, render: RENDER_ARG, fresh: FRESH_ARG, screenshot: SCREENSHOT_ARG },
|
|
114
233
|
required: ['url']
|
|
234
|
+
},
|
|
235
|
+
outputSchema: {
|
|
236
|
+
type: 'object',
|
|
237
|
+
properties: {
|
|
238
|
+
url: { type: 'string' },
|
|
239
|
+
images: {
|
|
240
|
+
type: 'array',
|
|
241
|
+
items: {
|
|
242
|
+
type: 'object',
|
|
243
|
+
properties: {
|
|
244
|
+
url: { type: 'string', description: 'Resolved absolute image URL' },
|
|
245
|
+
source: { type: 'string', description: 'img | srcset | picture | background | favicon | og' },
|
|
246
|
+
alt: { type: 'string', description: 'Alt text when present' }
|
|
247
|
+
},
|
|
248
|
+
required: ['url', 'source']
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
count: { type: 'number', description: 'Total images found (before any trimming)' },
|
|
252
|
+
usage: OUT_USAGE,
|
|
253
|
+
truncation_note: OUT_TRUNCATION
|
|
254
|
+
},
|
|
255
|
+
required: ['url'],
|
|
256
|
+
additionalProperties: true
|
|
115
257
|
}
|
|
116
258
|
},
|
|
117
259
|
{
|
|
@@ -124,6 +266,29 @@ const TOOLS = [
|
|
|
124
266
|
type: 'object',
|
|
125
267
|
properties: { url: { type: 'string', description: 'The website URL' }, render: RENDER_ARG, fresh: FRESH_ARG, screenshot: SCREENSHOT_ARG },
|
|
126
268
|
required: ['url']
|
|
269
|
+
},
|
|
270
|
+
outputSchema: {
|
|
271
|
+
type: 'object',
|
|
272
|
+
properties: {
|
|
273
|
+
url: { type: 'string' },
|
|
274
|
+
svgs: {
|
|
275
|
+
type: 'array',
|
|
276
|
+
items: {
|
|
277
|
+
type: 'object',
|
|
278
|
+
properties: {
|
|
279
|
+
type: { type: 'string', description: 'inline | file' },
|
|
280
|
+
code: { type: 'string', description: 'Sanitized paste-ready SVG markup (inline SVGs)' },
|
|
281
|
+
url: { type: 'string', description: 'File URL (external .svg references)' }
|
|
282
|
+
},
|
|
283
|
+
required: ['type']
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
count: { type: 'number', description: 'Total SVGs found (before any trimming)' },
|
|
287
|
+
usage: OUT_USAGE,
|
|
288
|
+
truncation_note: OUT_TRUNCATION
|
|
289
|
+
},
|
|
290
|
+
required: ['url'],
|
|
291
|
+
additionalProperties: true
|
|
127
292
|
}
|
|
128
293
|
},
|
|
129
294
|
{
|
|
@@ -140,6 +305,47 @@ const TOOLS = [
|
|
|
140
305
|
render: RENDER_ARG, fresh: FRESH_ARG, screenshot: SCREENSHOT_ARG
|
|
141
306
|
},
|
|
142
307
|
required: ['url']
|
|
308
|
+
},
|
|
309
|
+
outputSchema: {
|
|
310
|
+
type: 'object',
|
|
311
|
+
properties: {
|
|
312
|
+
url: { type: 'string' },
|
|
313
|
+
fonts: {
|
|
314
|
+
type: 'array',
|
|
315
|
+
description: 'Every font file / stylesheet found',
|
|
316
|
+
items: {
|
|
317
|
+
type: 'object',
|
|
318
|
+
properties: {
|
|
319
|
+
source: { type: 'string', description: 'font-face | google-fonts | preload' },
|
|
320
|
+
url: { type: 'string', description: 'The font file or stylesheet URL' },
|
|
321
|
+
family: { type: 'string' },
|
|
322
|
+
weight: { type: 'string', description: "'400', '700', or a variable range like '100 900'" },
|
|
323
|
+
style: { type: 'string' },
|
|
324
|
+
format: { type: 'string', description: 'woff2 | woff | ttf | otf | eot' }
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
families: {
|
|
329
|
+
type: 'array',
|
|
330
|
+
description: 'Grouped per-family summary - what most callers want',
|
|
331
|
+
items: {
|
|
332
|
+
type: 'object',
|
|
333
|
+
properties: {
|
|
334
|
+
family: { type: 'string' },
|
|
335
|
+
source: { type: 'string' },
|
|
336
|
+
weights: { type: 'array', items: { type: 'string' } },
|
|
337
|
+
styles: { type: 'array', items: { type: 'string' } },
|
|
338
|
+
files: { type: 'number' }
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
googleFontsLinks: { type: 'array', items: { type: 'string' } },
|
|
343
|
+
count: { type: 'number' },
|
|
344
|
+
usage: OUT_USAGE,
|
|
345
|
+
truncation_note: OUT_TRUNCATION
|
|
346
|
+
},
|
|
347
|
+
required: ['url'],
|
|
348
|
+
additionalProperties: true
|
|
143
349
|
}
|
|
144
350
|
},
|
|
145
351
|
{
|
|
@@ -152,6 +358,29 @@ const TOOLS = [
|
|
|
152
358
|
type: 'object',
|
|
153
359
|
properties: { url: { type: 'string', description: 'The website URL' }, render: RENDER_ARG, fresh: FRESH_ARG, screenshot: SCREENSHOT_ARG },
|
|
154
360
|
required: ['url']
|
|
361
|
+
},
|
|
362
|
+
outputSchema: {
|
|
363
|
+
type: 'object',
|
|
364
|
+
properties: {
|
|
365
|
+
url: { type: 'string' },
|
|
366
|
+
lotties: {
|
|
367
|
+
type: 'array',
|
|
368
|
+
items: {
|
|
369
|
+
type: 'object',
|
|
370
|
+
properties: {
|
|
371
|
+
url: { type: 'string', description: 'The animation JSON / .lottie file URL' },
|
|
372
|
+
source: { type: 'string', description: 'lottie-player | dotlottie-player | data-attr | json-ref' }
|
|
373
|
+
},
|
|
374
|
+
required: ['url']
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
count: { type: 'number' },
|
|
378
|
+
note: { type: 'string', description: 'Coverage caveat: only Lotties declared in the initial HTML are found' },
|
|
379
|
+
usage: OUT_USAGE,
|
|
380
|
+
truncation_note: OUT_TRUNCATION
|
|
381
|
+
},
|
|
382
|
+
required: ['url'],
|
|
383
|
+
additionalProperties: true
|
|
155
384
|
}
|
|
156
385
|
},
|
|
157
386
|
{
|
|
@@ -164,6 +393,30 @@ const TOOLS = [
|
|
|
164
393
|
type: 'object',
|
|
165
394
|
properties: { url: { type: 'string', description: 'The website URL' }, render: RENDER_ARG, fresh: FRESH_ARG, screenshot: SCREENSHOT_ARG },
|
|
166
395
|
required: ['url']
|
|
396
|
+
},
|
|
397
|
+
outputSchema: {
|
|
398
|
+
type: 'object',
|
|
399
|
+
properties: {
|
|
400
|
+
url: { type: 'string' },
|
|
401
|
+
videos: {
|
|
402
|
+
type: 'array',
|
|
403
|
+
items: {
|
|
404
|
+
type: 'object',
|
|
405
|
+
properties: {
|
|
406
|
+
url: { type: 'string', description: 'Resolved absolute video (or player page) URL' },
|
|
407
|
+
source: { type: 'string', description: 'video | source | og | embed | link. embed is a player page, not a file.' },
|
|
408
|
+
poster: { type: 'string', description: 'Poster image URL when the tag declared one' }
|
|
409
|
+
},
|
|
410
|
+
required: ['url', 'source']
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
count: { type: 'number', description: 'Total videos found (before any trimming)' },
|
|
414
|
+
note: { type: 'string', description: 'Coverage caveat: only videos declared in the initial HTML are found - use render=true for JS-mounted players' },
|
|
415
|
+
usage: OUT_USAGE,
|
|
416
|
+
truncation_note: OUT_TRUNCATION
|
|
417
|
+
},
|
|
418
|
+
required: ['url'],
|
|
419
|
+
additionalProperties: true
|
|
167
420
|
}
|
|
168
421
|
},
|
|
169
422
|
{
|
|
@@ -176,13 +429,36 @@ const TOOLS = [
|
|
|
176
429
|
type: 'object',
|
|
177
430
|
properties: { url: { type: 'string', description: 'The website URL' }, render: RENDER_ARG, fresh: FRESH_ARG, screenshot: SCREENSHOT_ARG },
|
|
178
431
|
required: ['url']
|
|
432
|
+
},
|
|
433
|
+
outputSchema: {
|
|
434
|
+
type: 'object',
|
|
435
|
+
properties: {
|
|
436
|
+
url: { type: 'string' },
|
|
437
|
+
audio: {
|
|
438
|
+
type: 'array',
|
|
439
|
+
items: {
|
|
440
|
+
type: 'object',
|
|
441
|
+
properties: {
|
|
442
|
+
url: { type: 'string', description: 'Resolved absolute audio file URL' },
|
|
443
|
+
source: { type: 'string', description: 'audio | source | og | link' }
|
|
444
|
+
},
|
|
445
|
+
required: ['url', 'source']
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
count: { type: 'number', description: 'Total audio files found (before any trimming)' },
|
|
449
|
+
note: { type: 'string', description: 'Coverage caveat: only audio declared in the initial HTML is found - use render=true for JS-mounted players' },
|
|
450
|
+
usage: OUT_USAGE,
|
|
451
|
+
truncation_note: OUT_TRUNCATION
|
|
452
|
+
},
|
|
453
|
+
required: ['url'],
|
|
454
|
+
additionalProperties: true
|
|
179
455
|
}
|
|
180
456
|
}
|
|
181
457
|
]
|
|
182
458
|
|
|
183
459
|
// Query params a tool may forward to the API, beyond `url`. Anything else in the
|
|
184
460
|
// tool arguments is ignored, so a hallucinated param can't reach the endpoint.
|
|
185
|
-
const FORWARDED_PARAMS = ['format', 'selector', 'framework', 'name', 'fields', 'render', 'fresh', 'screenshot']
|
|
461
|
+
const FORWARDED_PARAMS = ['format', 'selector', 'framework', 'name', 'theme', 'fields', 'render', 'fresh', 'screenshot']
|
|
186
462
|
|
|
187
463
|
async function callApi(endpoint, args) {
|
|
188
464
|
const q = new URLSearchParams({ url: args.url })
|
|
@@ -204,17 +480,43 @@ async function callApi(endpoint, args) {
|
|
|
204
480
|
return isJson ? JSON.stringify(body) : body
|
|
205
481
|
}
|
|
206
482
|
|
|
483
|
+
// Server-level guidance, sent once at initialize. Mirrors the `instructions`
|
|
484
|
+
// string the remote server (miromiro.app/mcp) returns, so a stdio client gets
|
|
485
|
+
// the same steering as a hosted one — chiefly: explain results, don't just dump
|
|
486
|
+
// them, and never present inferred brand colors as declared ones.
|
|
487
|
+
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.'
|
|
507
|
+
|
|
207
508
|
const server = new Server(
|
|
208
|
-
{ name: 'miromiro', version: '0.
|
|
209
|
-
{ capabilities: { tools: {} } }
|
|
509
|
+
{ name: 'miromiro', version: '0.6.0' },
|
|
510
|
+
{ capabilities: { tools: {} }, instructions: INSTRUCTIONS }
|
|
210
511
|
)
|
|
211
512
|
|
|
212
513
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
213
|
-
tools: TOOLS.map(({ name, title, description, inputSchema }) => ({
|
|
514
|
+
tools: TOOLS.map(({ name, title, description, inputSchema, outputSchema }) => ({
|
|
214
515
|
name,
|
|
215
516
|
title,
|
|
216
517
|
description,
|
|
217
518
|
inputSchema,
|
|
519
|
+
...(outputSchema ? { outputSchema } : {}),
|
|
218
520
|
annotations: { title, ...TOOL_ANNOTATIONS }
|
|
219
521
|
}))
|
|
220
522
|
}))
|
|
@@ -236,7 +538,18 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
236
538
|
}
|
|
237
539
|
try {
|
|
238
540
|
const out = await callApi(tool.endpoint, args)
|
|
239
|
-
|
|
541
|
+
// A tool that declares an outputSchema must also return structuredContent
|
|
542
|
+
// matching it (2025-06-18 spec), and structuredContent must be an object.
|
|
543
|
+
// format=css|tailwind on the tokens endpoint returns a raw code string —
|
|
544
|
+
// wrap it as { output }, which that tool's outputSchema declares.
|
|
545
|
+
let structured
|
|
546
|
+
try {
|
|
547
|
+
const parsed = JSON.parse(out)
|
|
548
|
+
structured = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : { output: parsed }
|
|
549
|
+
} catch {
|
|
550
|
+
structured = { output: out } // non-JSON body (css/tailwind block)
|
|
551
|
+
}
|
|
552
|
+
return { content: [{ type: 'text', text: out }], structuredContent: structured }
|
|
240
553
|
} catch (err) {
|
|
241
554
|
return { isError: true, content: [{ type: 'text', text: String(err.message || err) }] }
|
|
242
555
|
}
|