miromiro-mcp 0.1.0 → 0.2.1
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 +58 -17
- package/package.json +40 -32
- package/src/index.mjs +199 -176
package/README.md
CHANGED
|
@@ -1,11 +1,30 @@
|
|
|
1
1
|
# miromiro-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/miromiro-mcp)
|
|
4
|
+
[](./LICENSE)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
MCP server for the [MiroMiro](https://miromiro.app/api) design-extraction API. Give your AI agent the real design of any live website - brand colors, fonts, design tokens, SVGs, images, Lottie animations, and paste-ready component code - so it builds from actual values instead of guessing from a screenshot.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
Works with Claude (web, desktop, Claude Code), ChatGPT, Cursor, Windsurf, Codex, and any MCP client.
|
|
9
|
+
|
|
10
|
+
## Quick start: remote server (nothing to install)
|
|
11
|
+
|
|
12
|
+
A hosted server lives at:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
https://miromiro.app/mcp
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
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 (100 credits/month, no card).
|
|
19
|
+
|
|
20
|
+
- **Claude (web or desktop):** Settings → Connectors → Add custom connector → paste the URL. Or use the [one-click install](https://miromiro.app/api/mcp).
|
|
21
|
+
- **ChatGPT:** Settings → Security and login → enable Developer mode, then add the URL as a connector (authentication: OAuth).
|
|
22
|
+
- **Cursor (remote):** add `{ "mcpServers": { "miromiro": { "url": "https://miromiro.app/mcp" } } }` to `~/.cursor/mcp.json`.
|
|
23
|
+
- **Raw API key instead of OAuth:** append `?access_key=mm_live_...` to the URL, or send it as an `Authorization: Bearer` header.
|
|
24
|
+
|
|
25
|
+
## Local server (npm)
|
|
26
|
+
|
|
27
|
+
Prefer a local stdio process with the key in an env var? Get a free key at [miromiro.app/api/dashboard](https://miromiro.app/api/dashboard), then:
|
|
9
28
|
|
|
10
29
|
**Cursor** (`~/.cursor/mcp.json`) / **Windsurf** (Settings → MCP):
|
|
11
30
|
|
|
@@ -33,24 +52,46 @@ claude mcp add miromiro -s user \
|
|
|
33
52
|
|
|
34
53
|
| Tool | What it returns | Credits |
|
|
35
54
|
|---|---|---|
|
|
36
|
-
| `extract_code` | A section of a live page as a paste-ready component (Tailwind, HTML+CSS, JSX or Vue)
|
|
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` |
|
|
40
|
-
| `extract_lottie` | Lottie
|
|
41
|
-
| `extract_svgs` | Inline SVG code
|
|
42
|
-
| `extract_assets` | Every image URL on a page | 1 |
|
|
55
|
+
| `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 |
|
|
56
|
+
| `extract_brand` | Named color roles (primary/accent/background/text), palette, fonts split by role, logo, social profiles, and a 0-1 confidence score | 15 |
|
|
57
|
+
| `extract_design_tokens` | Full token system with usage counts; `format: css` or `tailwind` for paste-ready output | 10 |
|
|
58
|
+
| `extract_fonts` | Every font file and family the site loads, with weights, formats, and file URLs | 5 |
|
|
59
|
+
| `extract_lottie` | Lottie animation JSON files declared in the page | 3 |
|
|
60
|
+
| `extract_svgs` | Inline SVG code (sanitized, paste-ready) plus external SVG file URLs | 1 |
|
|
61
|
+
| `extract_assets` | Every image URL on a page, tagged by source, with alt text | 1 |
|
|
62
|
+
|
|
63
|
+
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.
|
|
43
64
|
|
|
44
|
-
|
|
65
|
+
### Controlling output size (remote server)
|
|
66
|
+
|
|
67
|
+
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:
|
|
68
|
+
|
|
69
|
+
- `limit` (up to 200) - raise the item cap: "give me 80 SVGs, not 20"
|
|
70
|
+
- `full_output: true` - complete long strings, like entire inline SVG markup
|
|
71
|
+
|
|
72
|
+
The REST API and this local npm server always return everything untrimmed.
|
|
73
|
+
|
|
74
|
+
## Examples
|
|
45
75
|
|
|
46
76
|
> "Rebuild stripe.com's pricing section in React"
|
|
47
77
|
|
|
48
|
-
The agent calls `extract_code` with the URL
|
|
78
|
+
The agent calls `extract_code` with the URL, gets the section's real markup, colors, fonts, and spacing, and adapts actual values instead of guessing.
|
|
79
|
+
|
|
80
|
+
> "Build a dashboard in the style of linear.app"
|
|
81
|
+
|
|
82
|
+
The agent calls `extract_brand` and `extract_design_tokens`, gets Linear's real colors, fonts, and spacing scale, and builds to match.
|
|
83
|
+
|
|
84
|
+
> "What fonts does apple.com use?"
|
|
85
|
+
|
|
86
|
+
The agent calls `extract_fonts` and returns every family with its font file URLs and weights.
|
|
87
|
+
|
|
88
|
+
## Notes
|
|
49
89
|
|
|
50
|
-
|
|
90
|
+
- The API reads static HTML and resolves the full CSS cascade. It does not execute JavaScript, so a fully client-rendered app may return little for images and Lottie (tokens, fonts, brand, and code come from CSS and are unaffected).
|
|
91
|
+
- Respect the sites you extract from: check their terms before reusing assets commercially.
|
|
51
92
|
|
|
52
|
-
|
|
93
|
+
## Links
|
|
53
94
|
|
|
54
|
-
|
|
95
|
+
[API docs](https://miromiro.app/api/docs) · [Playground (no account)](https://miromiro.app/api/demo) · [Pricing](https://miromiro.app/api/pricing) · [Changelog](https://miromiro.app/api/changelog) · [Connect page](https://miromiro.app/api/mcp)
|
|
55
96
|
|
|
56
|
-
|
|
97
|
+
MIT © [MiroMiro](https://miromiro.app)
|
package/package.json
CHANGED
|
@@ -1,32 +1,40 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "miromiro-mcp",
|
|
3
|
-
"version": "0.1
|
|
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
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "miromiro-mcp",
|
|
3
|
+
"version": "0.2.1",
|
|
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
|
+
"mcpName": "app.miromiro/miromiro",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"author": "MiroMiro (https://miromiro.app)",
|
|
8
|
+
"homepage": "https://miromiro.app/api",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/SoraiaBarroso/miromiro-mcp.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": { "url": "https://github.com/SoraiaBarroso/miromiro-mcp/issues" },
|
|
14
|
+
"bin": {
|
|
15
|
+
"miromiro-mcp": "src/index.mjs"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"mcp",
|
|
22
|
+
"model-context-protocol",
|
|
23
|
+
"mcp-server",
|
|
24
|
+
"design-tokens",
|
|
25
|
+
"design-to-code",
|
|
26
|
+
"brand-colors",
|
|
27
|
+
"web-scraping",
|
|
28
|
+
"miromiro",
|
|
29
|
+
"cursor",
|
|
30
|
+
"claude-code",
|
|
31
|
+
"windsurf"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
38
|
+
},
|
|
39
|
+
"license": "MIT"
|
|
40
|
+
}
|
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/
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
endpoint: '
|
|
59
|
-
description:
|
|
60
|
-
"
|
|
61
|
-
inputSchema: {
|
|
62
|
-
type: 'object',
|
|
63
|
-
properties: {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
},
|
|
103
|
-
required: ['url']
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
name: '
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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/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: 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.",
|
|
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.1' },
|
|
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')
|