mcp-openmockup 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.
- package/.env.example +8 -0
- package/README.md +60 -0
- package/dist/browser.d.ts +3 -0
- package/dist/browser.js +28 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +207 -0
- package/dist/projects.d.ts +22 -0
- package/dist/projects.js +94 -0
- package/dist/render.d.ts +47 -0
- package/dist/render.js +71 -0
- package/package.json +46 -0
package/.env.example
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Optional: override the OpenMockup URL (defaults to https://openmockup.dev)
|
|
2
|
+
# OPENMOCKUP_URL=https://openmockup.dev
|
|
3
|
+
|
|
4
|
+
# Required only for project tools (list_projects, get_project, create_project)
|
|
5
|
+
OPENMOCKUP_EMAIL=you@example.com
|
|
6
|
+
OPENMOCKUP_PASSWORD=yourpassword
|
|
7
|
+
OPENMOCKUP_SUPABASE_URL=https://your-project.supabase.co
|
|
8
|
+
OPENMOCKUP_SUPABASE_ANON_KEY=your-anon-key
|
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# mcp-openmockup
|
|
2
|
+
|
|
3
|
+
MCP server for [OpenMockup](https://openmockup.dev) — generate 3D device mockup images (iPhone, MacBook) from any AI agent.
|
|
4
|
+
|
|
5
|
+
No local OpenMockup install required. Rendering runs against `https://openmockup.dev`.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Add to your MCP config (`~/.cursor/mcp.json`, `~/.claude/.mcp.json`, etc.):
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"mcpServers": {
|
|
14
|
+
"mcp-openmockup": {
|
|
15
|
+
"command": "npx",
|
|
16
|
+
"args": ["-y", "mcp-openmockup"]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Requires [Node.js](https://nodejs.org/) 18+. On first render, Playwright downloads Chromium (~150 MB, one-time).
|
|
23
|
+
|
|
24
|
+
## Tools
|
|
25
|
+
|
|
26
|
+
### Render (no auth)
|
|
27
|
+
|
|
28
|
+
| Tool | Description |
|
|
29
|
+
|------|-------------|
|
|
30
|
+
| `render_mockup` | Single device mockup → PNG |
|
|
31
|
+
| `render_mockup_multi` | Multiple devices in one scene → PNG |
|
|
32
|
+
|
|
33
|
+
Pass `image_url` or `image_data` (base64). Optional: `device` (`phone`/`mac`), `device_color`, `bg_color`, `device_rotation`, `zoom`, `transparent`, `width`, `height`.
|
|
34
|
+
|
|
35
|
+
### Projects (optional auth)
|
|
36
|
+
|
|
37
|
+
| Tool | Description |
|
|
38
|
+
|------|-------------|
|
|
39
|
+
| `list_projects` | List saved projects |
|
|
40
|
+
| `get_project` | Get project by UUID |
|
|
41
|
+
| `create_project` | Create empty project |
|
|
42
|
+
|
|
43
|
+
Requires env vars:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
OPENMOCKUP_EMAIL=
|
|
47
|
+
OPENMOCKUP_PASSWORD=
|
|
48
|
+
OPENMOCKUP_SUPABASE_URL=
|
|
49
|
+
OPENMOCKUP_SUPABASE_ANON_KEY=
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Environment
|
|
53
|
+
|
|
54
|
+
| Variable | Default | Description |
|
|
55
|
+
|----------|---------|-------------|
|
|
56
|
+
| `OPENMOCKUP_URL` | `https://openmockup.dev` | Override renderer URL |
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { chromium } from 'playwright-chromium';
|
|
2
|
+
const OPENMOCKUP_URL = process.env.OPENMOCKUP_URL ?? 'https://openmockup.dev';
|
|
3
|
+
let browser = null;
|
|
4
|
+
let page = null;
|
|
5
|
+
export async function getPage() {
|
|
6
|
+
if (!browser || !browser.isConnected()) {
|
|
7
|
+
browser = await chromium.launch({
|
|
8
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
if (!page || page.isClosed()) {
|
|
12
|
+
page = await browser.newPage();
|
|
13
|
+
await page.setViewportSize({ width: 1440, height: 900 });
|
|
14
|
+
await page.goto(`${OPENMOCKUP_URL}?headless`, { waitUntil: 'networkidle' });
|
|
15
|
+
await page.waitForFunction(() => window.__rendererReady === true, {
|
|
16
|
+
timeout: 30_000,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return page;
|
|
20
|
+
}
|
|
21
|
+
export async function closeBrowser() {
|
|
22
|
+
await browser?.close();
|
|
23
|
+
browser = null;
|
|
24
|
+
page = null;
|
|
25
|
+
}
|
|
26
|
+
process.on('exit', () => void closeBrowser());
|
|
27
|
+
process.on('SIGINT', async () => { await closeBrowser(); process.exit(0); });
|
|
28
|
+
process.on('SIGTERM', async () => { await closeBrowser(); process.exit(0); });
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { renderMockup, renderMockupMulti } from './render.js';
|
|
6
|
+
import { listProjects, getProject, createProject } from './projects.js';
|
|
7
|
+
const server = new Server({ name: 'mcp-openmockup', version: '0.1.0' }, { capabilities: { tools: {} } });
|
|
8
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
9
|
+
tools: [
|
|
10
|
+
{
|
|
11
|
+
name: 'render_mockup',
|
|
12
|
+
description: 'Render a 3D device mockup image (iPhone or MacBook) with a screenshot on the screen. Returns a PNG image. No auth required.',
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
image_url: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
description: 'Public URL of the screenshot to display on the device screen',
|
|
19
|
+
},
|
|
20
|
+
image_data: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: 'Base64-encoded image (PNG or JPEG). Use this OR image_url.',
|
|
23
|
+
},
|
|
24
|
+
device: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
enum: ['phone', 'mac'],
|
|
27
|
+
default: 'phone',
|
|
28
|
+
description: 'Device type: iPhone (phone) or MacBook (mac)',
|
|
29
|
+
},
|
|
30
|
+
device_color: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
description: 'Hex color for the device frame. iPhone 17 colors: #DFCEEA (Lavender), #96AED1 (Mist Blue), #A9B689 (Sage), #353839 (Black), #F5F5F5 (White). iPhone 17 Pro: #32374A (Deep Blue), #F77E2D (Cosmic Orange), #F5F5F5 (Silver).',
|
|
33
|
+
},
|
|
34
|
+
bg_color: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
description: 'Background color (hex) or CSS gradient. E.g. "#0a0a0a", "#ffffff", "linear-gradient(135deg, #667eea 0%, #764ba2 100%)"',
|
|
37
|
+
},
|
|
38
|
+
device_rotation: {
|
|
39
|
+
type: 'array',
|
|
40
|
+
items: { type: 'number' },
|
|
41
|
+
minItems: 3,
|
|
42
|
+
maxItems: 3,
|
|
43
|
+
description: '[x, y, z] rotation in radians. E.g. [0, 0.3, 0] for a slight Y-axis rotation.',
|
|
44
|
+
},
|
|
45
|
+
zoom: {
|
|
46
|
+
type: 'number',
|
|
47
|
+
description: 'Camera zoom. 1 = default, >1 = closer, <1 = wider. Range 0.3–3.',
|
|
48
|
+
},
|
|
49
|
+
camera_offset_x: {
|
|
50
|
+
type: 'number',
|
|
51
|
+
description: 'Horizontal camera offset. Negative moves camera left (shows more right edge).',
|
|
52
|
+
},
|
|
53
|
+
camera_offset_y: {
|
|
54
|
+
type: 'number',
|
|
55
|
+
description: 'Vertical camera offset. Negative = lower camera (hero/contrapicado angle).',
|
|
56
|
+
},
|
|
57
|
+
camera_roll: {
|
|
58
|
+
type: 'number',
|
|
59
|
+
description: 'Camera roll in radians. 0 = upright, 1.5708 ≈ 90°.',
|
|
60
|
+
},
|
|
61
|
+
transparent: {
|
|
62
|
+
type: 'boolean',
|
|
63
|
+
description: 'Render with transparent background (alpha PNG). Ignores bg_color.',
|
|
64
|
+
},
|
|
65
|
+
width: {
|
|
66
|
+
type: 'number',
|
|
67
|
+
description: 'Output PNG width in pixels. Default: 1440.',
|
|
68
|
+
},
|
|
69
|
+
height: {
|
|
70
|
+
type: 'number',
|
|
71
|
+
description: 'Output PNG height in pixels. Default: 2880.',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'render_mockup_multi',
|
|
78
|
+
description: 'Render a scene with multiple 3D device mockups side by side. Returns a PNG image. No auth required.',
|
|
79
|
+
inputSchema: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
required: ['devices'],
|
|
82
|
+
properties: {
|
|
83
|
+
devices: {
|
|
84
|
+
type: 'array',
|
|
85
|
+
description: 'List of devices to include in the scene (1–4 recommended)',
|
|
86
|
+
items: {
|
|
87
|
+
type: 'object',
|
|
88
|
+
properties: {
|
|
89
|
+
image_url: {
|
|
90
|
+
type: 'string',
|
|
91
|
+
description: 'Public URL of the screenshot for this device',
|
|
92
|
+
},
|
|
93
|
+
image_data: {
|
|
94
|
+
type: 'string',
|
|
95
|
+
description: 'Base64 image data for this device',
|
|
96
|
+
},
|
|
97
|
+
kind: {
|
|
98
|
+
type: 'string',
|
|
99
|
+
enum: ['phone', 'mac'],
|
|
100
|
+
default: 'phone',
|
|
101
|
+
},
|
|
102
|
+
device_color: { type: 'string', description: 'Hex frame color' },
|
|
103
|
+
device_rotation: {
|
|
104
|
+
type: 'array',
|
|
105
|
+
items: { type: 'number' },
|
|
106
|
+
minItems: 3,
|
|
107
|
+
maxItems: 3,
|
|
108
|
+
description: '[x, y, z] in radians',
|
|
109
|
+
},
|
|
110
|
+
position_x: {
|
|
111
|
+
type: 'number',
|
|
112
|
+
description: 'Horizontal position in scene units. 0 = center, ±14 = adjacent devices.',
|
|
113
|
+
},
|
|
114
|
+
position_y: {
|
|
115
|
+
type: 'number',
|
|
116
|
+
description: 'Vertical position in scene units.',
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
bg_color: {
|
|
122
|
+
type: 'string',
|
|
123
|
+
description: 'Background color or CSS gradient',
|
|
124
|
+
},
|
|
125
|
+
zoom: {
|
|
126
|
+
type: 'number',
|
|
127
|
+
description: 'Camera zoom (0.3–3)',
|
|
128
|
+
},
|
|
129
|
+
camera_roll: {
|
|
130
|
+
type: 'number',
|
|
131
|
+
description: 'Camera roll in radians',
|
|
132
|
+
},
|
|
133
|
+
transparent: {
|
|
134
|
+
type: 'boolean',
|
|
135
|
+
description: 'Transparent background PNG',
|
|
136
|
+
},
|
|
137
|
+
width: {
|
|
138
|
+
type: 'number',
|
|
139
|
+
description: 'Output PNG width in pixels. Default: 2880.',
|
|
140
|
+
},
|
|
141
|
+
height: {
|
|
142
|
+
type: 'number',
|
|
143
|
+
description: 'Output PNG height in pixels. Default: 2880.',
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'list_projects',
|
|
150
|
+
description: 'List your saved OpenMockup projects. Requires OPENMOCKUP_EMAIL, OPENMOCKUP_PASSWORD, OPENMOCKUP_SUPABASE_URL, and OPENMOCKUP_SUPABASE_ANON_KEY env vars.',
|
|
151
|
+
inputSchema: { type: 'object', properties: {} },
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'get_project',
|
|
155
|
+
description: 'Get a specific OpenMockup project and its full scene snapshot by ID.',
|
|
156
|
+
inputSchema: {
|
|
157
|
+
type: 'object',
|
|
158
|
+
required: ['id'],
|
|
159
|
+
properties: {
|
|
160
|
+
id: { type: 'string', description: 'Project UUID' },
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'create_project',
|
|
166
|
+
description: 'Create a new empty OpenMockup project.',
|
|
167
|
+
inputSchema: {
|
|
168
|
+
type: 'object',
|
|
169
|
+
properties: {
|
|
170
|
+
name: { type: 'string', description: 'Project name' },
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
}));
|
|
176
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
177
|
+
const { name, arguments: args } = request.params;
|
|
178
|
+
try {
|
|
179
|
+
switch (name) {
|
|
180
|
+
case 'render_mockup':
|
|
181
|
+
return await renderMockup(args);
|
|
182
|
+
case 'render_mockup_multi':
|
|
183
|
+
return await renderMockupMulti(args);
|
|
184
|
+
case 'list_projects':
|
|
185
|
+
return await listProjects();
|
|
186
|
+
case 'get_project':
|
|
187
|
+
return await getProject(args);
|
|
188
|
+
case 'create_project':
|
|
189
|
+
return await createProject(args);
|
|
190
|
+
default:
|
|
191
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
return {
|
|
196
|
+
content: [
|
|
197
|
+
{
|
|
198
|
+
type: 'text',
|
|
199
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
isError: true,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
const transport = new StdioServerTransport();
|
|
207
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare function listProjects(): Promise<{
|
|
2
|
+
content: {
|
|
3
|
+
type: string;
|
|
4
|
+
text: string;
|
|
5
|
+
}[];
|
|
6
|
+
}>;
|
|
7
|
+
export declare function getProject({ id }: {
|
|
8
|
+
id: string;
|
|
9
|
+
}): Promise<{
|
|
10
|
+
content: {
|
|
11
|
+
type: string;
|
|
12
|
+
text: string;
|
|
13
|
+
}[];
|
|
14
|
+
}>;
|
|
15
|
+
export declare function createProject({ name }?: {
|
|
16
|
+
name?: string;
|
|
17
|
+
}): Promise<{
|
|
18
|
+
content: {
|
|
19
|
+
type: string;
|
|
20
|
+
text: string;
|
|
21
|
+
}[];
|
|
22
|
+
}>;
|
package/dist/projects.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
const SUPABASE_URL = process.env.OPENMOCKUP_SUPABASE_URL ?? '';
|
|
3
|
+
const SUPABASE_ANON_KEY = process.env.OPENMOCKUP_SUPABASE_ANON_KEY ?? '';
|
|
4
|
+
let supabase = null;
|
|
5
|
+
let userId = null;
|
|
6
|
+
async function getClient() {
|
|
7
|
+
if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
|
|
8
|
+
throw new Error('OPENMOCKUP_SUPABASE_URL and OPENMOCKUP_SUPABASE_ANON_KEY are required for project operations');
|
|
9
|
+
}
|
|
10
|
+
if (supabase)
|
|
11
|
+
return supabase;
|
|
12
|
+
supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
|
13
|
+
const email = process.env.OPENMOCKUP_EMAIL;
|
|
14
|
+
const password = process.env.OPENMOCKUP_PASSWORD;
|
|
15
|
+
if (!email || !password) {
|
|
16
|
+
throw new Error('OPENMOCKUP_EMAIL and OPENMOCKUP_PASSWORD are required for project operations');
|
|
17
|
+
}
|
|
18
|
+
const { data, error } = await supabase.auth.signInWithPassword({ email, password });
|
|
19
|
+
if (error)
|
|
20
|
+
throw new Error(`Auth failed: ${error.message}`);
|
|
21
|
+
userId = data.user.id;
|
|
22
|
+
return supabase;
|
|
23
|
+
}
|
|
24
|
+
export async function listProjects() {
|
|
25
|
+
const sb = await getClient();
|
|
26
|
+
const { data, error } = await sb
|
|
27
|
+
.from('projects')
|
|
28
|
+
.select('id, name, created_at, updated_at, is_public, thumbnail')
|
|
29
|
+
.eq('user_id', userId)
|
|
30
|
+
.order('updated_at', { ascending: false });
|
|
31
|
+
if (error)
|
|
32
|
+
throw new Error(error.message);
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export async function getProject({ id }) {
|
|
38
|
+
const sb = await getClient();
|
|
39
|
+
const { data, error } = await sb
|
|
40
|
+
.from('projects')
|
|
41
|
+
.select('*')
|
|
42
|
+
.eq('id', id)
|
|
43
|
+
.maybeSingle();
|
|
44
|
+
if (error)
|
|
45
|
+
throw new Error(error.message);
|
|
46
|
+
if (!data)
|
|
47
|
+
throw new Error(`Project not found: ${id}`);
|
|
48
|
+
return {
|
|
49
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export async function createProject({ name } = {}) {
|
|
53
|
+
const sb = await getClient();
|
|
54
|
+
const { data, error } = await sb
|
|
55
|
+
.from('projects')
|
|
56
|
+
.insert({
|
|
57
|
+
user_id: userId,
|
|
58
|
+
name: name?.trim() || 'Untitled mockup',
|
|
59
|
+
is_public: false,
|
|
60
|
+
snapshot: {
|
|
61
|
+
devices: [
|
|
62
|
+
{
|
|
63
|
+
id: crypto.randomUUID(),
|
|
64
|
+
screenshot: null,
|
|
65
|
+
screenMediaKind: null,
|
|
66
|
+
screenLoadError: null,
|
|
67
|
+
videoStartTime: 0,
|
|
68
|
+
videoEndTime: null,
|
|
69
|
+
deviceKind: 'phone',
|
|
70
|
+
deviceColor: '#DFCEEA',
|
|
71
|
+
deviceRotation: [0, 0, 0],
|
|
72
|
+
positionX: 0,
|
|
73
|
+
positionY: 0,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
bgColor: '#ffffff',
|
|
77
|
+
uiTheme: 'dark',
|
|
78
|
+
cameraRoll: 0,
|
|
79
|
+
orbitDistance: 28,
|
|
80
|
+
autoRotate: false,
|
|
81
|
+
cameraPosition: [0, 0, 28],
|
|
82
|
+
cameraTarget: [0, 0, 0],
|
|
83
|
+
viewportAspect: 1,
|
|
84
|
+
viewportInsetRight: 0,
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
.select()
|
|
88
|
+
.single();
|
|
89
|
+
if (error)
|
|
90
|
+
throw new Error(error.message);
|
|
91
|
+
return {
|
|
92
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
93
|
+
};
|
|
94
|
+
}
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type RenderOpts = {
|
|
2
|
+
image_url?: string;
|
|
3
|
+
image_data?: string;
|
|
4
|
+
device?: 'phone' | 'mac';
|
|
5
|
+
device_color?: string;
|
|
6
|
+
bg_color?: string;
|
|
7
|
+
device_rotation?: [number, number, number];
|
|
8
|
+
zoom?: number;
|
|
9
|
+
camera_offset_x?: number;
|
|
10
|
+
camera_offset_y?: number;
|
|
11
|
+
camera_roll?: number;
|
|
12
|
+
transparent?: boolean;
|
|
13
|
+
width?: number;
|
|
14
|
+
height?: number;
|
|
15
|
+
};
|
|
16
|
+
export type MultiDevice = {
|
|
17
|
+
image_url?: string;
|
|
18
|
+
image_data?: string;
|
|
19
|
+
kind?: 'phone' | 'mac';
|
|
20
|
+
device_color?: string;
|
|
21
|
+
device_rotation?: [number, number, number];
|
|
22
|
+
position_x?: number;
|
|
23
|
+
position_y?: number;
|
|
24
|
+
};
|
|
25
|
+
export type MultiRenderOpts = {
|
|
26
|
+
devices: MultiDevice[];
|
|
27
|
+
bg_color?: string;
|
|
28
|
+
zoom?: number;
|
|
29
|
+
camera_roll?: number;
|
|
30
|
+
transparent?: boolean;
|
|
31
|
+
width?: number;
|
|
32
|
+
height?: number;
|
|
33
|
+
};
|
|
34
|
+
export declare function renderMockup(opts: RenderOpts): Promise<{
|
|
35
|
+
content: {
|
|
36
|
+
type: string;
|
|
37
|
+
data: string;
|
|
38
|
+
mimeType: string;
|
|
39
|
+
}[];
|
|
40
|
+
}>;
|
|
41
|
+
export declare function renderMockupMulti(opts: MultiRenderOpts): Promise<{
|
|
42
|
+
content: {
|
|
43
|
+
type: string;
|
|
44
|
+
data: string;
|
|
45
|
+
mimeType: string;
|
|
46
|
+
}[];
|
|
47
|
+
}>;
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { getPage } from './browser.js';
|
|
2
|
+
async function toDataUrl(imageUrl, imageData) {
|
|
3
|
+
if (imageData) {
|
|
4
|
+
return imageData.startsWith('data:') ? imageData : `data:image/png;base64,${imageData}`;
|
|
5
|
+
}
|
|
6
|
+
if (imageUrl) {
|
|
7
|
+
if (imageUrl.startsWith('data:'))
|
|
8
|
+
return imageUrl;
|
|
9
|
+
const res = await fetch(imageUrl);
|
|
10
|
+
if (!res.ok)
|
|
11
|
+
throw new Error(`Failed to fetch image: ${res.status} ${res.statusText}`);
|
|
12
|
+
const buf = await res.arrayBuffer();
|
|
13
|
+
const base64 = Buffer.from(buf).toString('base64');
|
|
14
|
+
const ct = res.headers.get('content-type') ?? 'image/png';
|
|
15
|
+
return `data:${ct};base64,${base64}`;
|
|
16
|
+
}
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
function extractBase64(dataUrl) {
|
|
20
|
+
return dataUrl.replace(/^data:image\/\w+;base64,/, '');
|
|
21
|
+
}
|
|
22
|
+
export async function renderMockup(opts) {
|
|
23
|
+
const page = await getPage();
|
|
24
|
+
const imageDataUrl = await toDataUrl(opts.image_url, opts.image_data);
|
|
25
|
+
const renderOpts = {
|
|
26
|
+
devices: [
|
|
27
|
+
{
|
|
28
|
+
kind: opts.device ?? 'phone',
|
|
29
|
+
imageDataUrl,
|
|
30
|
+
deviceColor: opts.device_color,
|
|
31
|
+
deviceRotation: opts.device_rotation,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
bgColor: opts.bg_color,
|
|
35
|
+
zoom: opts.zoom,
|
|
36
|
+
camera_offset_x: opts.camera_offset_x,
|
|
37
|
+
camera_offset_y: opts.camera_offset_y,
|
|
38
|
+
camera_roll: opts.camera_roll,
|
|
39
|
+
transparent: opts.transparent,
|
|
40
|
+
width: opts.width ?? 1440,
|
|
41
|
+
height: opts.height ?? 2880,
|
|
42
|
+
};
|
|
43
|
+
const dataUrl = await page.evaluate(async (o) => window.renderMockup(o), renderOpts);
|
|
44
|
+
return {
|
|
45
|
+
content: [{ type: 'image', data: extractBase64(dataUrl), mimeType: 'image/png' }],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export async function renderMockupMulti(opts) {
|
|
49
|
+
const page = await getPage();
|
|
50
|
+
const devices = await Promise.all(opts.devices.map(async (d) => ({
|
|
51
|
+
kind: d.kind ?? 'phone',
|
|
52
|
+
imageDataUrl: await toDataUrl(d.image_url, d.image_data),
|
|
53
|
+
deviceColor: d.device_color,
|
|
54
|
+
deviceRotation: d.device_rotation,
|
|
55
|
+
positionX: d.position_x,
|
|
56
|
+
positionY: d.position_y,
|
|
57
|
+
})));
|
|
58
|
+
const renderOpts = {
|
|
59
|
+
devices,
|
|
60
|
+
bgColor: opts.bg_color,
|
|
61
|
+
zoom: opts.zoom,
|
|
62
|
+
camera_roll: opts.camera_roll,
|
|
63
|
+
transparent: opts.transparent,
|
|
64
|
+
width: opts.width ?? 2880,
|
|
65
|
+
height: opts.height ?? 2880,
|
|
66
|
+
};
|
|
67
|
+
const dataUrl = await page.evaluate(async (o) => window.renderMockup(o), renderOpts);
|
|
68
|
+
return {
|
|
69
|
+
content: [{ type: 'image', data: extractBase64(dataUrl), mimeType: 'image/png' }],
|
|
70
|
+
};
|
|
71
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-openmockup",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for OpenMockup — generate 3D device mockup images with AI",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"mockup",
|
|
8
|
+
"device",
|
|
9
|
+
"3d",
|
|
10
|
+
"screenshot",
|
|
11
|
+
"phone",
|
|
12
|
+
"mac",
|
|
13
|
+
"openmockup",
|
|
14
|
+
"iphone",
|
|
15
|
+
"macbook"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://openmockup.dev",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"mcp-openmockup": "dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md",
|
|
29
|
+
".env.example"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc",
|
|
33
|
+
"dev": "tsc --watch",
|
|
34
|
+
"start": "node dist/index.js",
|
|
35
|
+
"prepare": "npm run build"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
39
|
+
"@supabase/supabase-js": "^2.0.0",
|
|
40
|
+
"playwright-chromium": "^1.50.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^22.0.0",
|
|
44
|
+
"typescript": "^5.0.0"
|
|
45
|
+
}
|
|
46
|
+
}
|