@dynamic-mockups/mcp 1.0.1 → 1.0.3
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 +33 -47
- package/package.json +7 -3
- package/src/index.js +274 -39
package/README.md
CHANGED
|
@@ -13,15 +13,39 @@ Add the following to your MCP client configuration file:
|
|
|
13
13
|
|
|
14
14
|
```json
|
|
15
15
|
{
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
+
}
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Lovable
|
|
29
|
+
|
|
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))
|
|
33
|
+
|
|
34
|
+
### HTTP Transport
|
|
35
|
+
|
|
36
|
+
If you want to connect via HTTP instead of NPX, use:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
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
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
25
49
|
}
|
|
26
50
|
```
|
|
27
51
|
|
|
@@ -66,44 +90,6 @@ Ask your AI assistant:
|
|
|
66
90
|
| API info | "What are the rate limits and supported file formats for Dynamic Mockups?" |
|
|
67
91
|
| Print files | "Export print-ready files at 300 DPI for my poster mockup" |
|
|
68
92
|
|
|
69
|
-
## Development
|
|
70
|
-
|
|
71
|
-
### Local Installation
|
|
72
|
-
|
|
73
|
-
```bash
|
|
74
|
-
git clone https://github.com/dynamic-mockups/mcp.git
|
|
75
|
-
cd mcp-server
|
|
76
|
-
npm install
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### Run Locally
|
|
80
|
-
|
|
81
|
-
```bash
|
|
82
|
-
DYNAMIC_MOCKUPS_API_KEY=your_key npm start
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
### Development Mode (with auto-reload)
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
DYNAMIC_MOCKUPS_API_KEY=your_key npm run dev
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
### Use Local Version in MCP Client
|
|
92
|
-
|
|
93
|
-
```json
|
|
94
|
-
{
|
|
95
|
-
"mcpServers": {
|
|
96
|
-
"dynamic-mockups": {
|
|
97
|
-
"command": "node",
|
|
98
|
-
"args": ["/path/to/mcp-server/src/index.js"],
|
|
99
|
-
"env": {
|
|
100
|
-
"DYNAMIC_MOCKUPS_API_KEY": "your_api_key_here"
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
```
|
|
106
|
-
|
|
107
93
|
## Error Handling
|
|
108
94
|
|
|
109
95
|
The server returns clear error messages for common issues:
|
|
@@ -122,4 +108,4 @@ The server returns clear error messages for common issues:
|
|
|
122
108
|
|
|
123
109
|
## License
|
|
124
110
|
|
|
125
|
-
MIT
|
|
111
|
+
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.3",
|
|
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,25 +88,36 @@ 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: "
|
|
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.",
|
|
102
121
|
get_key_at: "https://app.dynamicmockups.com/dashboard-api",
|
|
103
122
|
}
|
|
104
123
|
);
|
|
@@ -106,6 +125,34 @@ function validateApiKey() {
|
|
|
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);
|