@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.
Files changed (3) hide show
  1. package/README.md +79 -124
  2. package/package.json +7 -3
  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 (Model Context Protocol) server for the [Dynamic Mockups API](https://dynamicmockups.com). Generate product mockups directly from AI assistants like Claude, Cursor, Windsurf, and more.
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
- ## Installation
6
-
7
- ### Quick Start with npx
5
+ ## Requirements
8
6
 
9
- No installation required - just configure your MCP client:
7
+ - Node.js 18 or higher
8
+ - Dynamic Mockups API key — [get one here](https://app.dynamicmockups.com/dashboard-api)
10
9
 
11
- ```json
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
- Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
12
+ Add the following to your MCP client configuration file:
36
13
 
37
14
  ```json
38
15
  {
39
- "mcpServers": {
40
- "dynamic-mockups": {
41
- "command": "npx",
42
- "args": ["-y", "@dynamic-mockups/mcp"],
43
- "env": {
44
- "DYNAMIC_MOCKUPS_API_KEY": "your_api_key_here"
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
- ### Claude Code (CLI)
28
+ ### Lovable
52
29
 
53
- Add to `.mcp.json` in your project root:
54
-
55
- ```json
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
- ### Cursor
34
+ ### HTTP Transport
70
35
 
71
- Add to `.cursor/mcp.json` in your project:
36
+ If you want to connect via HTTP instead of NPX, use:
72
37
 
73
38
  ```json
74
39
  {
75
- "mcpServers": {
76
- "dynamic-mockups": {
77
- "command": "npx",
78
- "args": ["-y", "@dynamic-mockups/mcp"],
79
- "env": {
80
- "DYNAMIC_MOCKUPS_API_KEY": "your_api_key_here"
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
- ## Available Tools
106
-
107
- ### API Information
108
- - **`get_api_info`** - Get API knowledge base (billing, rate limits, formats, best practices, support)
109
-
110
- ### Catalogs
111
- - **`get_catalogs`** - Retrieve all available catalogs
112
-
113
- ### Collections
114
- - **`get_collections`** - Retrieve collections (optionally filter by catalog)
115
- - **`create_collection`** - Create a new collection
116
-
117
- ### Mockups
118
- - **`get_mockups`** - Get list of available mockups with optional filters
119
- - **`get_mockup_by_uuid`** - Retrieve a specific mockup by UUID
120
-
121
- ### Rendering
122
- - **`create_render`** - Create a single mockup render with design assets
123
- - **`create_batch_render`** - Render multiple mockups in one request
124
- - **`export_print_files`** - Export print files for smart objects
125
-
126
- ### PSD Files
127
- - **`upload_psd`** - Upload a PSD file with optional mockup template creation
128
- - **`delete_psd`** - Delete a PSD file with optional related mockups deletion
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
- > "Show me all mockups in my T-shirt collection"
140
-
141
- ### Create a Render
142
-
143
- > "Create a mockup render using mockup UUID abc123 with my logo from https://example.com/logo.png"
144
-
145
- ### Batch Render
146
-
147
- > "Render my design on all mockups in the Summer collection"
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/dynamicmockups/mcp-server.git
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/account/api-keys)
200
- - [GitHub Issues](https://github.com/dynamicmockups/mcp-server/issues)
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.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
- "dev": "node --watch src/index.js"
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
- function createApiClient() {
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": 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
- function validateApiKey() {
97
- if (!API_KEY) {
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: "Set the DYNAMIC_MOCKUPS_API_KEY environment variable in your MCP client configuration.",
102
- get_key_at: "https://app.dynamicmockups.com/account/api-keys",
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 error = validateApiKey();
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 error = validateApiKey();
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 error = validateApiKey();
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 error = validateApiKey();
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 error = validateApiKey();
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 error = validateApiKey();
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 error = validateApiKey();
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 error = validateApiKey();
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 error = validateApiKey();
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 error = validateApiKey();
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
- return await handler(args || {});
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
- async function main() {
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);