@dynamic-mockups/mcp 1.0.0 → 1.0.2
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 +79 -124
- package/package.json +7 -3
- package/src/index.js +275 -40
package/README.md
CHANGED
|
@@ -1,171 +1,125 @@
|
|
|
1
1
|
# Dynamic Mockups MCP Server
|
|
2
2
|
|
|
3
|
-
Official MCP
|
|
3
|
+
Official MCP server for [Dynamic Mockups](https://dynamicmockups.com) — a product mockup generator API. Create professional mockups directly from AI assistants like Claude, Cursor, and Windsurf.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
### Quick Start with npx
|
|
5
|
+
## Requirements
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
- Node.js 18 or higher
|
|
8
|
+
- Dynamic Mockups API key — [get one here](https://app.dynamicmockups.com/dashboard-api)
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
{
|
|
13
|
-
"mcpServers": {
|
|
14
|
-
"dynamic-mockups": {
|
|
15
|
-
"command": "npx",
|
|
16
|
-
"args": ["-y", "@dynamic-mockups/mcp"],
|
|
17
|
-
"env": {
|
|
18
|
-
"DYNAMIC_MOCKUPS_API_KEY": "your_api_key_here"
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
### Get Your API Key
|
|
26
|
-
|
|
27
|
-
1. Go to [Dynamic Mockups Dashboard](https://app.dynamicmockups.com/account/api-keys)
|
|
28
|
-
2. Create a new API key
|
|
29
|
-
3. Add it to your MCP client configuration
|
|
30
|
-
|
|
31
|
-
## Configuration by Client
|
|
32
|
-
|
|
33
|
-
### Claude Desktop
|
|
10
|
+
## Installation
|
|
34
11
|
|
|
35
|
-
|
|
12
|
+
Add the following to your MCP client configuration file:
|
|
36
13
|
|
|
37
14
|
```json
|
|
38
15
|
{
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"dynamic-mockups": {
|
|
18
|
+
"command": "npx",
|
|
19
|
+
"args": ["-y", "@dynamic-mockups/mcp"],
|
|
20
|
+
"env": {
|
|
21
|
+
"DYNAMIC_MOCKUPS_API_KEY": "your_api_key_here"
|
|
22
|
+
}
|
|
45
23
|
}
|
|
46
|
-
|
|
47
|
-
}
|
|
24
|
+
}
|
|
48
25
|
}
|
|
49
26
|
```
|
|
50
27
|
|
|
51
|
-
###
|
|
28
|
+
### Lovable
|
|
52
29
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
{
|
|
57
|
-
"mcpServers": {
|
|
58
|
-
"dynamic-mockups": {
|
|
59
|
-
"command": "npx",
|
|
60
|
-
"args": ["-y", "@dynamic-mockups/mcp"],
|
|
61
|
-
"env": {
|
|
62
|
-
"DYNAMIC_MOCKUPS_API_KEY": "your_api_key_here"
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
```
|
|
30
|
+
For Lovable, simply enter:
|
|
31
|
+
- **Server URL**: `https://mcp.dynamicmockups.com`
|
|
32
|
+
- **API Key**: Your Dynamic Mockups API key ([get one here](https://app.dynamicmockups.com/dashboard-api))
|
|
68
33
|
|
|
69
|
-
###
|
|
34
|
+
### HTTP Transport
|
|
70
35
|
|
|
71
|
-
|
|
36
|
+
If you want to connect via HTTP instead of NPX, use:
|
|
72
37
|
|
|
73
38
|
```json
|
|
74
39
|
{
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### Windsurf
|
|
88
|
-
|
|
89
|
-
Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
90
|
-
|
|
91
|
-
```json
|
|
92
|
-
{
|
|
93
|
-
"mcpServers": {
|
|
94
|
-
"dynamic-mockups": {
|
|
95
|
-
"command": "npx",
|
|
96
|
-
"args": ["-y", "@dynamic-mockups/mcp"],
|
|
97
|
-
"env": {
|
|
98
|
-
"DYNAMIC_MOCKUPS_API_KEY": "your_api_key_here"
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"dynamic-mockups": {
|
|
42
|
+
"type": "http",
|
|
43
|
+
"url": "https://mcp.dynamicmockups.com",
|
|
44
|
+
"headers": {
|
|
45
|
+
"x-api-key": "your_api_key_here"
|
|
46
|
+
}
|
|
99
47
|
}
|
|
100
|
-
|
|
101
|
-
}
|
|
48
|
+
}
|
|
102
49
|
}
|
|
103
50
|
```
|
|
104
51
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
52
|
+
### Config File Locations
|
|
53
|
+
|
|
54
|
+
| Client | Config File Path |
|
|
55
|
+
|--------|------------------|
|
|
56
|
+
| Claude Desktop (macOS) | `~/Library/Application Support/Claude/claude_desktop_config.json` |
|
|
57
|
+
| Claude Desktop (Windows) | `%APPDATA%\Claude\claude_desktop_config.json` |
|
|
58
|
+
| Claude Code (CLI) | `.mcp.json` in project root |
|
|
59
|
+
| Cursor | `.cursor/mcp.json` in project |
|
|
60
|
+
| Windsurf | `~/.codeium/windsurf/mcp_config.json` |
|
|
61
|
+
|
|
62
|
+
## Tools
|
|
63
|
+
|
|
64
|
+
| Tool | Description |
|
|
65
|
+
|------|-------------|
|
|
66
|
+
| `get_api_info` | Get API knowledge base (billing, rate limits, formats, best practices, support) |
|
|
67
|
+
| `get_catalogs` | Retrieve all available catalogs |
|
|
68
|
+
| `get_collections` | Retrieve collections (optionally filter by catalog) |
|
|
69
|
+
| `create_collection` | Create a new collection |
|
|
70
|
+
| `get_mockups` | Get list of available mockups with optional filters |
|
|
71
|
+
| `get_mockup_by_uuid` | Retrieve a specific mockup by UUID |
|
|
72
|
+
| `create_render` | Create a single mockup render with design assets (1 credit) |
|
|
73
|
+
| `create_batch_render` | Render multiple mockups in one request (1 credit per image) |
|
|
74
|
+
| `export_print_files` | Export high-resolution print files for production |
|
|
75
|
+
| `upload_psd` | Upload a PSD file and optionally create a mockup template |
|
|
76
|
+
| `delete_psd` | Delete a PSD file with optional related mockups deletion |
|
|
129
77
|
|
|
130
78
|
## Usage Examples
|
|
131
79
|
|
|
132
|
-
### Get Your Catalogs
|
|
133
|
-
|
|
134
80
|
Ask your AI assistant:
|
|
135
|
-
> "Get my Dynamic Mockups catalogs"
|
|
136
|
-
|
|
137
|
-
### Get Mockups from a Collection
|
|
138
81
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
82
|
+
| Use Case | Example Prompt |
|
|
83
|
+
|----------|----------------|
|
|
84
|
+
| List catalogs | "Get my Dynamic Mockups catalogs" |
|
|
85
|
+
| Browse mockups | "Show me all mockups in my T-shirt collection" |
|
|
86
|
+
| Single render | "Create a mockup render using any T-shirt mockup with my artwork from url: https://example.com/my-design.png" |
|
|
87
|
+
| Batch render | "Render my artwork from url: https://example.com/my-design.png on all mockups in the Winter T-shirt collection" |
|
|
88
|
+
| Create collection | "Create a new collection called Summer 2025 Hoodies" |
|
|
89
|
+
| Upload PSD | "Upload my PSD mockup from url: https://example.com/my-mockup.psd and create a template from it" |
|
|
90
|
+
| API info | "What are the rate limits and supported file formats for Dynamic Mockups?" |
|
|
91
|
+
| Print files | "Export print-ready files at 300 DPI for my poster mockup" |
|
|
148
92
|
|
|
149
93
|
## Development
|
|
150
94
|
|
|
151
95
|
### Local Installation
|
|
152
96
|
|
|
153
97
|
```bash
|
|
154
|
-
git clone https://github.com/
|
|
98
|
+
git clone https://github.com/dynamic-mockups/mcp.git
|
|
155
99
|
cd mcp-server
|
|
156
100
|
npm install
|
|
157
101
|
```
|
|
158
102
|
|
|
159
|
-
### Run Locally
|
|
103
|
+
### Run Locally (stdio mode - default)
|
|
160
104
|
|
|
161
105
|
```bash
|
|
162
106
|
DYNAMIC_MOCKUPS_API_KEY=your_key npm start
|
|
163
107
|
```
|
|
164
108
|
|
|
109
|
+
### Run Locally (HTTP mode)
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
DYNAMIC_MOCKUPS_API_KEY=your_key npm run start:http
|
|
113
|
+
```
|
|
114
|
+
|
|
165
115
|
### Development Mode (with auto-reload)
|
|
166
116
|
|
|
167
117
|
```bash
|
|
118
|
+
# stdio mode
|
|
168
119
|
DYNAMIC_MOCKUPS_API_KEY=your_key npm run dev
|
|
120
|
+
|
|
121
|
+
# HTTP mode
|
|
122
|
+
DYNAMIC_MOCKUPS_API_KEY=your_key npm run dev:http
|
|
169
123
|
```
|
|
170
124
|
|
|
171
125
|
### Use Local Version in MCP Client
|
|
@@ -196,9 +150,10 @@ The server returns clear error messages for common issues:
|
|
|
196
150
|
|
|
197
151
|
- [Dynamic Mockups Website](https://dynamicmockups.com)
|
|
198
152
|
- [API Documentation](https://docs.dynamicmockups.com)
|
|
199
|
-
- [Get API Key](https://app.dynamicmockups.com/
|
|
200
|
-
- [GitHub
|
|
153
|
+
- [Get API Key](https://app.dynamicmockups.com/dashboard-api)
|
|
154
|
+
- [GitHub Repository](https://github.com/dynamic-mockups/mcp)
|
|
155
|
+
- [GitHub Issues](https://github.com/dynamic-mockups/mcp/issues)
|
|
201
156
|
|
|
202
157
|
## License
|
|
203
158
|
|
|
204
|
-
MIT
|
|
159
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dynamic-mockups/mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Official Dynamic Mockups MCP Server - Generate product mockups with AI assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node src/index.js",
|
|
12
|
-
"
|
|
12
|
+
"start:http": "node src/index.js --http",
|
|
13
|
+
"dev": "node --watch src/index.js",
|
|
14
|
+
"dev:http": "node --watch src/index.js --http"
|
|
13
15
|
},
|
|
14
16
|
"keywords": [
|
|
15
17
|
"mcp",
|
|
@@ -34,7 +36,9 @@
|
|
|
34
36
|
},
|
|
35
37
|
"dependencies": {
|
|
36
38
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
37
|
-
"axios": "^1.6.0"
|
|
39
|
+
"axios": "^1.6.0",
|
|
40
|
+
"express": "^4.21.0",
|
|
41
|
+
"cors": "^2.8.5"
|
|
38
42
|
},
|
|
39
43
|
"engines": {
|
|
40
44
|
"node": ">=18.0.0"
|
package/src/index.js
CHANGED
|
@@ -4,11 +4,19 @@
|
|
|
4
4
|
* Dynamic Mockups MCP Server
|
|
5
5
|
* Official MCP server for the Dynamic Mockups API
|
|
6
6
|
* https://dynamicmockups.com
|
|
7
|
+
*
|
|
8
|
+
* Supports both stdio and HTTP/SSE transports:
|
|
9
|
+
* - stdio: Default when run directly (for Claude Desktop, Cursor, etc.)
|
|
10
|
+
* - HTTP/SSE: When imported and used with startHttpServer() (for web-based clients)
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
13
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
10
14
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
11
15
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
17
|
+
import express from "express";
|
|
18
|
+
import cors from "cors";
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
12
20
|
import axios from "axios";
|
|
13
21
|
import { ResponseFormatter } from "./response-formatter.js";
|
|
14
22
|
|
|
@@ -80,32 +88,71 @@ const server = new Server(
|
|
|
80
88
|
// HTTP Client
|
|
81
89
|
// =============================================================================
|
|
82
90
|
|
|
83
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Creates an API client with the provided API key.
|
|
93
|
+
* For stdio transport: uses environment variable
|
|
94
|
+
* For HTTP transport: uses client-provided API key from Authorization header
|
|
95
|
+
*
|
|
96
|
+
* @param {string} apiKey - The API key to use for requests
|
|
97
|
+
*/
|
|
98
|
+
function createApiClient(apiKey) {
|
|
84
99
|
return axios.create({
|
|
85
100
|
baseURL: API_BASE_URL,
|
|
86
101
|
headers: {
|
|
87
102
|
"Accept": "application/json",
|
|
88
103
|
"Content-Type": "application/json",
|
|
89
|
-
"x-api-key":
|
|
104
|
+
"x-api-key": apiKey || "",
|
|
90
105
|
},
|
|
91
106
|
timeout: 60000, // 60 second timeout for render operations
|
|
92
107
|
validateStatus: (status) => status < 500, // Only throw on 5xx errors
|
|
93
108
|
});
|
|
94
109
|
}
|
|
95
110
|
|
|
96
|
-
|
|
97
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Validates that an API key is present.
|
|
113
|
+
* @param {string} apiKey - The API key to validate
|
|
114
|
+
*/
|
|
115
|
+
function validateApiKey(apiKey) {
|
|
116
|
+
if (!apiKey) {
|
|
98
117
|
return ResponseFormatter.error(
|
|
99
118
|
"API key not configured",
|
|
100
119
|
{
|
|
101
|
-
solution: "
|
|
102
|
-
get_key_at: "https://app.dynamicmockups.com/
|
|
120
|
+
solution: "Provide your Dynamic Mockups API key. For HTTP transport, use the Authorization header (Bearer token). For stdio transport, set the DYNAMIC_MOCKUPS_API_KEY environment variable.",
|
|
121
|
+
get_key_at: "https://app.dynamicmockups.com/dashboard-api",
|
|
103
122
|
}
|
|
104
123
|
);
|
|
105
124
|
}
|
|
106
125
|
return null;
|
|
107
126
|
}
|
|
108
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Extracts the API key from various sources.
|
|
130
|
+
* Priority: requestInfo headers > environment variable
|
|
131
|
+
*
|
|
132
|
+
* @param {Object} extra - Extra info passed to handlers (contains requestInfo for HTTP transport)
|
|
133
|
+
*/
|
|
134
|
+
function getApiKey(extra) {
|
|
135
|
+
// For HTTP transport: check Authorization header (Bearer token) or x-api-key header
|
|
136
|
+
if (extra?.requestInfo?.headers) {
|
|
137
|
+
const headers = extra.requestInfo.headers;
|
|
138
|
+
|
|
139
|
+
// Check Authorization: Bearer <token>
|
|
140
|
+
const authHeader = headers.authorization || headers.Authorization;
|
|
141
|
+
if (authHeader && authHeader.startsWith("Bearer ")) {
|
|
142
|
+
return authHeader.slice(7);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check x-api-key header
|
|
146
|
+
const apiKeyHeader = headers["x-api-key"] || headers["X-Api-Key"];
|
|
147
|
+
if (apiKeyHeader) {
|
|
148
|
+
return apiKeyHeader;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Fallback to environment variable (for stdio transport)
|
|
153
|
+
return API_KEY;
|
|
154
|
+
}
|
|
155
|
+
|
|
109
156
|
// =============================================================================
|
|
110
157
|
// Tool Definitions
|
|
111
158
|
// =============================================================================
|
|
@@ -491,20 +538,22 @@ async function handleGetApiInfo(args) {
|
|
|
491
538
|
return ResponseFormatter.ok(topicMap[topic] || API_KNOWLEDGE_BASE);
|
|
492
539
|
}
|
|
493
540
|
|
|
494
|
-
async function handleGetCatalogs() {
|
|
495
|
-
const
|
|
541
|
+
async function handleGetCatalogs(args, extra) {
|
|
542
|
+
const apiKey = getApiKey(extra);
|
|
543
|
+
const error = validateApiKey(apiKey);
|
|
496
544
|
if (error) return error;
|
|
497
545
|
|
|
498
546
|
try {
|
|
499
|
-
const response = await createApiClient().get("/catalogs");
|
|
547
|
+
const response = await createApiClient(apiKey).get("/catalogs");
|
|
500
548
|
return ResponseFormatter.fromApiResponse(response);
|
|
501
549
|
} catch (err) {
|
|
502
550
|
return ResponseFormatter.fromError(err, "Failed to get catalogs");
|
|
503
551
|
}
|
|
504
552
|
}
|
|
505
553
|
|
|
506
|
-
async function handleGetCollections(args = {}) {
|
|
507
|
-
const
|
|
554
|
+
async function handleGetCollections(args = {}, extra) {
|
|
555
|
+
const apiKey = getApiKey(extra);
|
|
556
|
+
const error = validateApiKey(apiKey);
|
|
508
557
|
if (error) return error;
|
|
509
558
|
|
|
510
559
|
try {
|
|
@@ -514,30 +563,32 @@ async function handleGetCollections(args = {}) {
|
|
|
514
563
|
params.append("include_all_catalogs", args.include_all_catalogs);
|
|
515
564
|
}
|
|
516
565
|
|
|
517
|
-
const response = await createApiClient().get(`/collections?${params}`);
|
|
566
|
+
const response = await createApiClient(apiKey).get(`/collections?${params}`);
|
|
518
567
|
return ResponseFormatter.fromApiResponse(response);
|
|
519
568
|
} catch (err) {
|
|
520
569
|
return ResponseFormatter.fromError(err, "Failed to get collections");
|
|
521
570
|
}
|
|
522
571
|
}
|
|
523
572
|
|
|
524
|
-
async function handleCreateCollection(args) {
|
|
525
|
-
const
|
|
573
|
+
async function handleCreateCollection(args, extra) {
|
|
574
|
+
const apiKey = getApiKey(extra);
|
|
575
|
+
const error = validateApiKey(apiKey);
|
|
526
576
|
if (error) return error;
|
|
527
577
|
|
|
528
578
|
try {
|
|
529
579
|
const payload = { name: args.name };
|
|
530
580
|
if (args.catalog_uuid) payload.catalog_uuid = args.catalog_uuid;
|
|
531
581
|
|
|
532
|
-
const response = await createApiClient().post("/collections", payload);
|
|
582
|
+
const response = await createApiClient(apiKey).post("/collections", payload);
|
|
533
583
|
return ResponseFormatter.fromApiResponse(response, `Collection "${args.name}" created`);
|
|
534
584
|
} catch (err) {
|
|
535
585
|
return ResponseFormatter.fromError(err, "Failed to create collection");
|
|
536
586
|
}
|
|
537
587
|
}
|
|
538
588
|
|
|
539
|
-
async function handleGetMockups(args = {}) {
|
|
540
|
-
const
|
|
589
|
+
async function handleGetMockups(args = {}, extra) {
|
|
590
|
+
const apiKey = getApiKey(extra);
|
|
591
|
+
const error = validateApiKey(apiKey);
|
|
541
592
|
if (error) return error;
|
|
542
593
|
|
|
543
594
|
try {
|
|
@@ -549,27 +600,29 @@ async function handleGetMockups(args = {}) {
|
|
|
549
600
|
}
|
|
550
601
|
if (args.name) params.append("name", args.name);
|
|
551
602
|
|
|
552
|
-
const response = await createApiClient().get(`/mockups?${params}`);
|
|
603
|
+
const response = await createApiClient(apiKey).get(`/mockups?${params}`);
|
|
553
604
|
return ResponseFormatter.fromApiResponse(response);
|
|
554
605
|
} catch (err) {
|
|
555
606
|
return ResponseFormatter.fromError(err, "Failed to get mockups");
|
|
556
607
|
}
|
|
557
608
|
}
|
|
558
609
|
|
|
559
|
-
async function handleGetMockupByUuid(args) {
|
|
560
|
-
const
|
|
610
|
+
async function handleGetMockupByUuid(args, extra) {
|
|
611
|
+
const apiKey = getApiKey(extra);
|
|
612
|
+
const error = validateApiKey(apiKey);
|
|
561
613
|
if (error) return error;
|
|
562
614
|
|
|
563
615
|
try {
|
|
564
|
-
const response = await createApiClient().get(`/mockup/${args.uuid}`);
|
|
616
|
+
const response = await createApiClient(apiKey).get(`/mockup/${args.uuid}`);
|
|
565
617
|
return ResponseFormatter.fromApiResponse(response);
|
|
566
618
|
} catch (err) {
|
|
567
619
|
return ResponseFormatter.fromError(err, "Failed to get mockup");
|
|
568
620
|
}
|
|
569
621
|
}
|
|
570
622
|
|
|
571
|
-
async function handleCreateRender(args) {
|
|
572
|
-
const
|
|
623
|
+
async function handleCreateRender(args, extra) {
|
|
624
|
+
const apiKey = getApiKey(extra);
|
|
625
|
+
const error = validateApiKey(apiKey);
|
|
573
626
|
if (error) return error;
|
|
574
627
|
|
|
575
628
|
try {
|
|
@@ -581,22 +634,23 @@ async function handleCreateRender(args) {
|
|
|
581
634
|
if (args.export_options) payload.export_options = args.export_options;
|
|
582
635
|
if (args.text_layers) payload.text_layers = args.text_layers;
|
|
583
636
|
|
|
584
|
-
const response = await createApiClient().post("/renders", payload);
|
|
637
|
+
const response = await createApiClient(apiKey).post("/renders", payload);
|
|
585
638
|
return ResponseFormatter.fromApiResponse(response, "Render created (1 credit used)");
|
|
586
639
|
} catch (err) {
|
|
587
640
|
return ResponseFormatter.fromError(err, "Failed to create render");
|
|
588
641
|
}
|
|
589
642
|
}
|
|
590
643
|
|
|
591
|
-
async function handleCreateBatchRender(args) {
|
|
592
|
-
const
|
|
644
|
+
async function handleCreateBatchRender(args, extra) {
|
|
645
|
+
const apiKey = getApiKey(extra);
|
|
646
|
+
const error = validateApiKey(apiKey);
|
|
593
647
|
if (error) return error;
|
|
594
648
|
|
|
595
649
|
try {
|
|
596
650
|
const payload = { renders: args.renders };
|
|
597
651
|
if (args.export_options) payload.export_options = args.export_options;
|
|
598
652
|
|
|
599
|
-
const response = await createApiClient().post("/renders/batch", payload);
|
|
653
|
+
const response = await createApiClient(apiKey).post("/renders/batch", payload);
|
|
600
654
|
const count = args.renders?.length || 0;
|
|
601
655
|
return ResponseFormatter.fromApiResponse(response, `Batch render complete (${count} credits used)`);
|
|
602
656
|
} catch (err) {
|
|
@@ -604,8 +658,9 @@ async function handleCreateBatchRender(args) {
|
|
|
604
658
|
}
|
|
605
659
|
}
|
|
606
660
|
|
|
607
|
-
async function handleExportPrintFiles(args) {
|
|
608
|
-
const
|
|
661
|
+
async function handleExportPrintFiles(args, extra) {
|
|
662
|
+
const apiKey = getApiKey(extra);
|
|
663
|
+
const error = validateApiKey(apiKey);
|
|
609
664
|
if (error) return error;
|
|
610
665
|
|
|
611
666
|
try {
|
|
@@ -617,15 +672,16 @@ async function handleExportPrintFiles(args) {
|
|
|
617
672
|
if (args.export_options) payload.export_options = args.export_options;
|
|
618
673
|
if (args.text_layers) payload.text_layers = args.text_layers;
|
|
619
674
|
|
|
620
|
-
const response = await createApiClient().post("/renders/print-files", payload);
|
|
675
|
+
const response = await createApiClient(apiKey).post("/renders/print-files", payload);
|
|
621
676
|
return ResponseFormatter.fromApiResponse(response, "Print files exported");
|
|
622
677
|
} catch (err) {
|
|
623
678
|
return ResponseFormatter.fromError(err, "Failed to export print files");
|
|
624
679
|
}
|
|
625
680
|
}
|
|
626
681
|
|
|
627
|
-
async function handleUploadPsd(args) {
|
|
628
|
-
const
|
|
682
|
+
async function handleUploadPsd(args, extra) {
|
|
683
|
+
const apiKey = getApiKey(extra);
|
|
684
|
+
const error = validateApiKey(apiKey);
|
|
629
685
|
if (error) return error;
|
|
630
686
|
|
|
631
687
|
try {
|
|
@@ -634,15 +690,16 @@ async function handleUploadPsd(args) {
|
|
|
634
690
|
if (args.psd_category_id) payload.psd_category_id = args.psd_category_id;
|
|
635
691
|
if (args.mockup_template) payload.mockup_template = args.mockup_template;
|
|
636
692
|
|
|
637
|
-
const response = await createApiClient().post("/psd/upload", payload);
|
|
693
|
+
const response = await createApiClient(apiKey).post("/psd/upload", payload);
|
|
638
694
|
return ResponseFormatter.fromApiResponse(response, "PSD uploaded successfully");
|
|
639
695
|
} catch (err) {
|
|
640
696
|
return ResponseFormatter.fromError(err, "Failed to upload PSD");
|
|
641
697
|
}
|
|
642
698
|
}
|
|
643
699
|
|
|
644
|
-
async function handleDeletePsd(args) {
|
|
645
|
-
const
|
|
700
|
+
async function handleDeletePsd(args, extra) {
|
|
701
|
+
const apiKey = getApiKey(extra);
|
|
702
|
+
const error = validateApiKey(apiKey);
|
|
646
703
|
if (error) return error;
|
|
647
704
|
|
|
648
705
|
try {
|
|
@@ -651,7 +708,7 @@ async function handleDeletePsd(args) {
|
|
|
651
708
|
payload.delete_related_mockups = args.delete_related_mockups;
|
|
652
709
|
}
|
|
653
710
|
|
|
654
|
-
const response = await createApiClient().post("/psd/delete", payload);
|
|
711
|
+
const response = await createApiClient(apiKey).post("/psd/delete", payload);
|
|
655
712
|
return ResponseFormatter.fromApiResponse(response, "PSD deleted successfully");
|
|
656
713
|
} catch (err) {
|
|
657
714
|
return ResponseFormatter.fromError(err, "Failed to delete PSD");
|
|
@@ -682,7 +739,7 @@ const toolHandlers = {
|
|
|
682
739
|
|
|
683
740
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
684
741
|
|
|
685
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
742
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
686
743
|
const { name, arguments: args } = request.params;
|
|
687
744
|
|
|
688
745
|
const handler = toolHandlers[name];
|
|
@@ -691,7 +748,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
691
748
|
}
|
|
692
749
|
|
|
693
750
|
try {
|
|
694
|
-
|
|
751
|
+
// Pass extra context (contains requestInfo with headers for HTTP transport)
|
|
752
|
+
return await handler(args || {}, extra);
|
|
695
753
|
} catch (err) {
|
|
696
754
|
return ResponseFormatter.fromError(err, `Error executing ${name}`);
|
|
697
755
|
}
|
|
@@ -701,12 +759,189 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
701
759
|
// Server Startup
|
|
702
760
|
// =============================================================================
|
|
703
761
|
|
|
704
|
-
|
|
762
|
+
/**
|
|
763
|
+
* Start the MCP server with stdio transport (default)
|
|
764
|
+
* Used by: Claude Desktop, Claude Code, Cursor, Windsurf
|
|
765
|
+
*/
|
|
766
|
+
async function startStdioServer() {
|
|
705
767
|
const transport = new StdioServerTransport();
|
|
706
768
|
await server.connect(transport);
|
|
707
|
-
console.error(`Dynamic Mockups MCP Server v${SERVER_VERSION} running`);
|
|
769
|
+
console.error(`Dynamic Mockups MCP Server v${SERVER_VERSION} running (stdio)`);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Start the MCP server with Streamable HTTP transport
|
|
774
|
+
* Used by: Web-based clients like Lovable that require a URL endpoint
|
|
775
|
+
*
|
|
776
|
+
* Uses the modern StreamableHTTPServerTransport which supports both
|
|
777
|
+
* SSE streaming and direct HTTP responses per the MCP specification.
|
|
778
|
+
*
|
|
779
|
+
* @param {Object} options - Server options
|
|
780
|
+
* @param {number} options.port - Port to listen on (default: 3000)
|
|
781
|
+
* @param {string} options.host - Host to bind to (default: '0.0.0.0')
|
|
782
|
+
* @param {string|string[]} options.corsOrigin - CORS origin(s) (default: '*')
|
|
783
|
+
* @returns {Promise<{app: Express, httpServer: Server}>}
|
|
784
|
+
*/
|
|
785
|
+
async function startHttpServer(options = {}) {
|
|
786
|
+
const {
|
|
787
|
+
port = process.env.PORT || 3000,
|
|
788
|
+
host = process.env.HOST || "0.0.0.0",
|
|
789
|
+
corsOrigin = process.env.CORS_ORIGIN || "*",
|
|
790
|
+
} = options;
|
|
791
|
+
|
|
792
|
+
const app = express();
|
|
793
|
+
|
|
794
|
+
// CORS configuration - must allow MCP-specific headers and auth headers
|
|
795
|
+
app.use(cors({
|
|
796
|
+
origin: corsOrigin,
|
|
797
|
+
methods: ["GET", "POST", "DELETE", "OPTIONS"],
|
|
798
|
+
allowedHeaders: [
|
|
799
|
+
"Content-Type",
|
|
800
|
+
"Accept",
|
|
801
|
+
"Authorization",
|
|
802
|
+
"x-api-key",
|
|
803
|
+
"Mcp-Session-Id",
|
|
804
|
+
"Last-Event-Id",
|
|
805
|
+
"Mcp-Protocol-Version",
|
|
806
|
+
],
|
|
807
|
+
exposedHeaders: ["Mcp-Session-Id"],
|
|
808
|
+
credentials: true,
|
|
809
|
+
}));
|
|
810
|
+
|
|
811
|
+
// Note: We don't use express.json() globally because StreamableHTTPServerTransport
|
|
812
|
+
// needs to read the raw body. We parse JSON only for non-MCP endpoints.
|
|
813
|
+
|
|
814
|
+
// Store active transports by session ID for multi-session support
|
|
815
|
+
const transports = new Map();
|
|
816
|
+
|
|
817
|
+
// Health check endpoint
|
|
818
|
+
app.get("/health", (req, res) => {
|
|
819
|
+
res.json({
|
|
820
|
+
status: "ok",
|
|
821
|
+
server: SERVER_NAME,
|
|
822
|
+
version: SERVER_VERSION,
|
|
823
|
+
transport: "streamable-http",
|
|
824
|
+
activeSessions: transports.size,
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
// API info endpoint (convenience endpoint, not MCP)
|
|
829
|
+
app.get("/api/info", (req, res) => {
|
|
830
|
+
res.json({
|
|
831
|
+
server: SERVER_NAME,
|
|
832
|
+
version: SERVER_VERSION,
|
|
833
|
+
api_key_configured: !!API_KEY,
|
|
834
|
+
tools: tools.map((t) => ({ name: t.name, description: t.description })),
|
|
835
|
+
endpoints: {
|
|
836
|
+
mcp: "/mcp",
|
|
837
|
+
health: "/health",
|
|
838
|
+
},
|
|
839
|
+
});
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
// MCP endpoint - handles all MCP communication (GET for SSE, POST for messages, DELETE for session termination)
|
|
843
|
+
// Available at both "/" and "/mcp" for flexibility
|
|
844
|
+
app.all(["/", "/mcp"], async (req, res) => {
|
|
845
|
+
// Check for existing session
|
|
846
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
847
|
+
|
|
848
|
+
if (sessionId && transports.has(sessionId)) {
|
|
849
|
+
// Reuse existing transport for this session
|
|
850
|
+
const { transport } = transports.get(sessionId);
|
|
851
|
+
await transport.handleRequest(req, res);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// For new connections (no session ID or unknown session), create new transport
|
|
856
|
+
if (req.method === "POST" || req.method === "GET") {
|
|
857
|
+
// Create a new MCP server instance for this connection
|
|
858
|
+
const connectionServer = new Server(
|
|
859
|
+
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
860
|
+
{ capabilities: { tools: {} } }
|
|
861
|
+
);
|
|
862
|
+
|
|
863
|
+
// Register the same handlers
|
|
864
|
+
connectionServer.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
865
|
+
connectionServer.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
866
|
+
const { name, arguments: args } = request.params;
|
|
867
|
+
const handler = toolHandlers[name];
|
|
868
|
+
if (!handler) {
|
|
869
|
+
return ResponseFormatter.error(`Unknown tool: ${name}`);
|
|
870
|
+
}
|
|
871
|
+
try {
|
|
872
|
+
// Pass extra context (contains requestInfo with headers for API key extraction)
|
|
873
|
+
return await handler(args || {}, extra);
|
|
874
|
+
} catch (err) {
|
|
875
|
+
return ResponseFormatter.fromError(err, `Error executing ${name}`);
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
// Create Streamable HTTP transport with session support
|
|
880
|
+
const transport = new StreamableHTTPServerTransport({
|
|
881
|
+
sessionIdGenerator: () => randomUUID(),
|
|
882
|
+
onsessioninitialized: (newSessionId) => {
|
|
883
|
+
console.error(`Session initialized: ${newSessionId}`);
|
|
884
|
+
transports.set(newSessionId, { transport, server: connectionServer });
|
|
885
|
+
},
|
|
886
|
+
onsessionclosed: (closedSessionId) => {
|
|
887
|
+
console.error(`Session closed: ${closedSessionId}`);
|
|
888
|
+
transports.delete(closedSessionId);
|
|
889
|
+
},
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
// Connect server to transport
|
|
893
|
+
await connectionServer.connect(transport);
|
|
894
|
+
|
|
895
|
+
// Handle the request
|
|
896
|
+
await transport.handleRequest(req, res);
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// Unknown session for DELETE or other methods
|
|
901
|
+
res.status(400).json({
|
|
902
|
+
jsonrpc: "2.0",
|
|
903
|
+
error: {
|
|
904
|
+
code: -32000,
|
|
905
|
+
message: "Bad Request: No valid session found",
|
|
906
|
+
},
|
|
907
|
+
id: null,
|
|
908
|
+
});
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
// Legacy SSE endpoint for backwards compatibility
|
|
912
|
+
app.get("/sse", (req, res) => {
|
|
913
|
+
res.redirect(307, "/");
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
const httpServer = app.listen(port, host, () => {
|
|
917
|
+
console.error(`Dynamic Mockups MCP Server v${SERVER_VERSION} running`);
|
|
918
|
+
console.error(`Streamable HTTP transport available at http://${host}:${port}`);
|
|
919
|
+
console.error(` - MCP endpoint: http://${host}:${port}/mcp`);
|
|
920
|
+
console.error(` - Health check: http://${host}:${port}/health`);
|
|
921
|
+
console.error(` - API info: http://${host}:${port}/api/info`);
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
return { app, httpServer };
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Main entry point - determines transport based on command line args or environment
|
|
929
|
+
*/
|
|
930
|
+
async function main() {
|
|
931
|
+
const args = process.argv.slice(2);
|
|
932
|
+
const useHttp = args.includes("--http") || process.env.MCP_TRANSPORT === "http";
|
|
933
|
+
|
|
934
|
+
if (useHttp) {
|
|
935
|
+
await startHttpServer();
|
|
936
|
+
} else {
|
|
937
|
+
await startStdioServer();
|
|
938
|
+
}
|
|
708
939
|
}
|
|
709
940
|
|
|
941
|
+
// Export for programmatic use
|
|
942
|
+
export { startHttpServer, startStdioServer, server, tools, toolHandlers };
|
|
943
|
+
|
|
944
|
+
// Run if executed directly
|
|
710
945
|
main().catch((err) => {
|
|
711
946
|
console.error("Fatal error:", err);
|
|
712
947
|
process.exit(1);
|