mapbox-create-map-mcp 1.2.0 → 1.3.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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/popui.js +209 -0
  3. package/src/server.js +250 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mapbox-create-map-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "A Mapbox-based MCP tool for creating geographic data visualizations",
5
5
  "main": "dist/index.js",
6
6
  "module": "src/index.js",
package/src/popui.js ADDED
@@ -0,0 +1,209 @@
1
+ /**
2
+ * CreatePopup - 弹框样式配置生成工具
3
+ * 用于生成Mapbox弹框(Popup)的样式配置
4
+ */
5
+
6
+ class CreatePopup {
7
+ constructor() {
8
+ /**
9
+ * 参数元数据定义
10
+ */
11
+ this.metadata = {
12
+ name: {
13
+ type: 'string',
14
+ required: true,
15
+ default: '默认样式',
16
+ description: '弹框样式名称'
17
+ },
18
+ closeButton: {
19
+ type: 'boolean',
20
+ required: false,
21
+ default: true,
22
+ description: '是否显示右上角的关闭按钮'
23
+ },
24
+ closeOnClick: {
25
+ type: 'boolean',
26
+ required: false,
27
+ default: true,
28
+ description: '点击地图任意地方时是否自动关闭Popup'
29
+ },
30
+ closeOnMove: {
31
+ type: 'boolean',
32
+ required: false,
33
+ default: false,
34
+ description: '地图移动(拖动或缩放)时是否自动关闭Popup'
35
+ },
36
+ anchor: {
37
+ type: 'string',
38
+ required: false,
39
+ default: 'top',
40
+ enum: ['auto', 'center', 'top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right'],
41
+ description: 'Popup箭头朝向锚点位置'
42
+ },
43
+ offset: {
44
+ type: 'array',
45
+ required: false,
46
+ default: [0, 0],
47
+ description: '偏移量,用于微调Popup位置,格式为[x, y],单位为像素'
48
+ },
49
+ maxWidth: {
50
+ type: 'number',
51
+ required: false,
52
+ default: 240,
53
+ min: 100,
54
+ max: 800,
55
+ description: '弹框最大宽度,单位为像素(px)'
56
+ },
57
+ className: {
58
+ type: 'string',
59
+ required: false,
60
+ default: '',
61
+ description: '给Popup添加自定义CSS class名'
62
+ },
63
+ altitude: {
64
+ type: 'number',
65
+ required: false,
66
+ default: 0,
67
+ min: 0,
68
+ max: 10000,
69
+ description: '弹窗在地图表面上方的海拔高度(米)'
70
+ },
71
+ htmlContent: {
72
+ type: 'string',
73
+ required: true,
74
+ default: '<div style="padding: 10px;"><h3>标题</h3><p>内容</p></div>',
75
+ description: '弹框的HTML内容,支持完整的HTML标签和内联样式'
76
+ },
77
+ theme: {
78
+ type: 'string',
79
+ required: false,
80
+ default: 'light',
81
+ enum: ['light', 'dark', 'minimal', 'custom'],
82
+ description: '预设主题样式:light(浅色)、dark(深色)、minimal(简约)、custom(自定义)'
83
+ }
84
+ };
85
+
86
+ /**
87
+ * 预设主题样式模板
88
+ */
89
+ this.themeTemplates = {
90
+ light: {
91
+ className: 'light-popup',
92
+ htmlWrapper: '<div style="padding: 10px; background: #fff; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">{{content}}</div>'
93
+ },
94
+ dark: {
95
+ className: 'dark-popup',
96
+ htmlWrapper: '<div style="padding: 15px; background: #2d2d2d; color: #fff; border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.3);">{{content}}</div>'
97
+ },
98
+ minimal: {
99
+ className: 'minimal-popup',
100
+ htmlWrapper: '<div style="padding: 8px 12px; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 2px; font-size: 12px;">{{content}}</div>'
101
+ },
102
+ custom: {
103
+ className: '',
104
+ htmlWrapper: '{{content}}'
105
+ }
106
+ };
107
+ }
108
+
109
+ /**
110
+ * 验证参数
111
+ */
112
+ validate(config) {
113
+ const errors = [];
114
+
115
+ // 验证必填参数
116
+ if (!config.name || typeof config.name !== 'string') {
117
+ errors.push('name 是必填字段,且必须是字符串');
118
+ }
119
+
120
+ if (!config.htmlContent || typeof config.htmlContent !== 'string') {
121
+ errors.push('htmlContent 是必填字段,且必须是字符串');
122
+ }
123
+
124
+ // 验证anchor枚举值
125
+ if (config.anchor) {
126
+ const validAnchors = ['auto', 'center', 'top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right'];
127
+ if (!validAnchors.includes(config.anchor)) {
128
+ errors.push(`anchor 必须是以下值之一: ${validAnchors.join(', ')}`);
129
+ }
130
+ }
131
+
132
+ // 验证theme枚举值
133
+ if (config.theme) {
134
+ const validThemes = ['light', 'dark', 'minimal', 'custom'];
135
+ if (!validThemes.includes(config.theme)) {
136
+ errors.push(`theme 必须是以下值之一: ${validThemes.join(', ')}`);
137
+ }
138
+ }
139
+
140
+ // 验证offset格式
141
+ if (config.offset) {
142
+ if (!Array.isArray(config.offset) || config.offset.length !== 2) {
143
+ errors.push('offset 必须是包含两个数字的数组 [x, y]');
144
+ } else if (typeof config.offset[0] !== 'number' || typeof config.offset[1] !== 'number') {
145
+ errors.push('offset 数组中的值必须是数字');
146
+ }
147
+ }
148
+
149
+ // 验证maxWidth范围
150
+ if (config.maxWidth !== undefined) {
151
+ if (typeof config.maxWidth !== 'number' || config.maxWidth < 100 || config.maxWidth > 800) {
152
+ errors.push('maxWidth 必须是100到800之间的数字');
153
+ }
154
+ }
155
+
156
+ // 验证altitude范围
157
+ if (config.altitude !== undefined) {
158
+ if (typeof config.altitude !== 'number' || config.altitude < 0 || config.altitude > 10000) {
159
+ errors.push('altitude 必须是0到10000之间的数字');
160
+ }
161
+ }
162
+
163
+ return errors;
164
+ }
165
+
166
+ /**
167
+ * 创建弹框配置
168
+ * @param {Object} config - 弹框配置参数
169
+ * @returns {Promise<Object>} 弹框配置对象
170
+ */
171
+ async create(config) {
172
+ // 验证参数
173
+ const errors = this.validate(config);
174
+ if (errors.length > 0) {
175
+ throw new Error(`参数验证失败: ${errors.join('; ')}`);
176
+ }
177
+
178
+ // 获取主题模板
179
+ const theme = config.theme || 'light';
180
+ const themeTemplate = this.themeTemplates[theme] || this.themeTemplates.light;
181
+
182
+ // 应用主题包装(如果htmlContent不是完整的HTML结构)
183
+ let finalHtmlContent = config.htmlContent;
184
+ if (theme !== 'custom' && !config.htmlContent.includes('style=')) {
185
+ finalHtmlContent = themeTemplate.htmlWrapper.replace('{{content}}', config.htmlContent);
186
+ }
187
+
188
+ // 构建弹框配置对象
189
+ const popupConfig = {
190
+ name: config.name || this.metadata.name.default,
191
+ closeButton: config.closeButton !== undefined ? config.closeButton : this.metadata.closeButton.default,
192
+ closeOnClick: config.closeOnClick !== undefined ? config.closeOnClick : this.metadata.closeOnClick.default,
193
+ closeOnMove: config.closeOnMove !== undefined ? config.closeOnMove : this.metadata.closeOnMove.default,
194
+ anchor: config.anchor || this.metadata.anchor.default,
195
+ offset: config.offset || this.metadata.offset.default,
196
+ maxWidth: config.maxWidth || this.metadata.maxWidth.default,
197
+ className: config.className || themeTemplate.className || this.metadata.className.default,
198
+ altitude: config.altitude !== undefined ? config.altitude : this.metadata.altitude.default,
199
+ htmlContent: finalHtmlContent
200
+ };
201
+
202
+ return {
203
+ success: true,
204
+ config: popupConfig
205
+ };
206
+ }
207
+ }
208
+
209
+ export default CreatePopup;
package/src/server.js CHANGED
@@ -19,9 +19,178 @@ try {
19
19
  process.exit(1);
20
20
  }
21
21
 
22
+ /**
23
+ * CreatePopup - 弹框样式配置生成工具
24
+ */
25
+ class CreatePopup {
26
+ constructor() {
27
+ this.metadata = {
28
+ name: {
29
+ type: 'string',
30
+ required: true,
31
+ default: '默认样式',
32
+ description: '弹框样式名称'
33
+ },
34
+ closeButton: {
35
+ type: 'boolean',
36
+ required: false,
37
+ default: true,
38
+ description: '是否显示右上角的关闭按钮'
39
+ },
40
+ closeOnClick: {
41
+ type: 'boolean',
42
+ required: false,
43
+ default: true,
44
+ description: '点击地图任意地方时是否自动关闭Popup'
45
+ },
46
+ closeOnMove: {
47
+ type: 'boolean',
48
+ required: false,
49
+ default: false,
50
+ description: '地图移动(拖动或缩放)时是否自动关闭Popup'
51
+ },
52
+ anchor: {
53
+ type: 'string',
54
+ required: false,
55
+ default: 'top',
56
+ enum: ['auto', 'center', 'top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right'],
57
+ description: 'Popup箭头朝向锚点位置'
58
+ },
59
+ offset: {
60
+ type: 'array',
61
+ required: false,
62
+ default: [0, 0],
63
+ description: '偏移量,用于微调Popup位置,格式为[x, y],单位为像素'
64
+ },
65
+ maxWidth: {
66
+ type: 'number',
67
+ required: false,
68
+ default: 240,
69
+ min: 100,
70
+ max: 800,
71
+ description: '弹框最大宽度,单位为像素(px)'
72
+ },
73
+ className: {
74
+ type: 'string',
75
+ required: false,
76
+ default: '',
77
+ description: '给Popup添加自定义CSS class名'
78
+ },
79
+ altitude: {
80
+ type: 'number',
81
+ required: false,
82
+ default: 0,
83
+ min: 0,
84
+ max: 10000,
85
+ description: '弹窗在地图表面上方的海拔高度(米)'
86
+ },
87
+ htmlContent: {
88
+ type: 'string',
89
+ required: true,
90
+ default: '<div style="padding: 10px;"><h3>标题</h3><p>内容</p></div>',
91
+ description: '弹框的HTML内容,支持完整的HTML标签和内联样式'
92
+ },
93
+ theme: {
94
+ type: 'string',
95
+ required: false,
96
+ default: 'light',
97
+ enum: ['light', 'dark', 'minimal', 'custom'],
98
+ description: '预设主题样式:light(浅色)、dark(深色)、minimal(简约)、custom(自定义)'
99
+ }
100
+ };
101
+
102
+ this.themeTemplates = {
103
+ light: {
104
+ className: 'light-popup',
105
+ htmlWrapper: '<div style="padding: 10px; background: #fff; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">{{content}}</div>'
106
+ },
107
+ dark: {
108
+ className: 'dark-popup',
109
+ htmlWrapper: '<div style="padding: 15px; background: #2d2d2d; color: #fff; border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.3);">{{content}}</div>'
110
+ },
111
+ minimal: {
112
+ className: 'minimal-popup',
113
+ htmlWrapper: '<div style="padding: 8px 12px; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 2px; font-size: 12px;">{{content}}</div>'
114
+ },
115
+ custom: {
116
+ className: '',
117
+ htmlWrapper: '{{content}}'
118
+ }
119
+ };
120
+ }
121
+
122
+ validate(config) {
123
+ const errors = [];
124
+ if (!config.name || typeof config.name !== 'string') {
125
+ errors.push('name 是必填字段,且必须是字符串');
126
+ }
127
+ if (!config.htmlContent || typeof config.htmlContent !== 'string') {
128
+ errors.push('htmlContent 是必填字段,且必须是字符串');
129
+ }
130
+ if (config.anchor) {
131
+ const validAnchors = ['auto', 'center', 'top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right'];
132
+ if (!validAnchors.includes(config.anchor)) {
133
+ errors.push(`anchor 必须是以下值之一: ${validAnchors.join(', ')}`);
134
+ }
135
+ }
136
+ if (config.theme) {
137
+ const validThemes = ['light', 'dark', 'minimal', 'custom'];
138
+ if (!validThemes.includes(config.theme)) {
139
+ errors.push(`theme 必须是以下值之一: ${validThemes.join(', ')}`);
140
+ }
141
+ }
142
+ if (config.offset) {
143
+ if (!Array.isArray(config.offset) || config.offset.length !== 2) {
144
+ errors.push('offset 必须是包含两个数字的数组 [x, y]');
145
+ }
146
+ }
147
+ if (config.maxWidth !== undefined) {
148
+ if (typeof config.maxWidth !== 'number' || config.maxWidth < 100 || config.maxWidth > 800) {
149
+ errors.push('maxWidth 必须是100到800之间的数字');
150
+ }
151
+ }
152
+ if (config.altitude !== undefined) {
153
+ if (typeof config.altitude !== 'number' || config.altitude < 0 || config.altitude > 10000) {
154
+ errors.push('altitude 必须是0到10000之间的数字');
155
+ }
156
+ }
157
+ return errors;
158
+ }
159
+
160
+ async create(config) {
161
+ const errors = this.validate(config);
162
+ if (errors.length > 0) {
163
+ throw new Error(`参数验证失败: ${errors.join('; ')}`);
164
+ }
165
+ const theme = config.theme || 'light';
166
+ const themeTemplate = this.themeTemplates[theme] || this.themeTemplates.light;
167
+ let finalHtmlContent = config.htmlContent;
168
+ if (theme !== 'custom' && !config.htmlContent.includes('style=')) {
169
+ finalHtmlContent = themeTemplate.htmlWrapper.replace('{{content}}', config.htmlContent);
170
+ }
171
+ const popupConfig = {
172
+ name: config.name || this.metadata.name.default,
173
+ closeButton: config.closeButton !== undefined ? config.closeButton : this.metadata.closeButton.default,
174
+ closeOnClick: config.closeOnClick !== undefined ? config.closeOnClick : this.metadata.closeOnClick.default,
175
+ closeOnMove: config.closeOnMove !== undefined ? config.closeOnMove : this.metadata.closeOnMove.default,
176
+ anchor: config.anchor || this.metadata.anchor.default,
177
+ offset: config.offset || this.metadata.offset.default,
178
+ maxWidth: config.maxWidth || this.metadata.maxWidth.default,
179
+ className: config.className || themeTemplate.className || this.metadata.className.default,
180
+ altitude: config.altitude !== undefined ? config.altitude : this.metadata.altitude.default,
181
+ htmlContent: finalHtmlContent
182
+ };
183
+ return {
184
+ success: true,
185
+ config: popupConfig
186
+ };
187
+ }
188
+ }
189
+
22
190
  class McpServer {
23
191
  constructor() {
24
192
  this.createMap = new CreateMap();
193
+ this.createPopup = new CreatePopup();
25
194
  this.serverInfo = {
26
195
  name: 'mapbox-create-map-mcp',
27
196
  version: '1.0.0',
@@ -62,6 +231,12 @@ class McpServer {
62
231
  async handleRequest(request) {
63
232
  const { id, method, params } = request;
64
233
 
234
+ // 通知类型的消息不需要返回响应
235
+ if (method && method.startsWith('notifications/')) {
236
+ // 处理通知,不返回任何内容
237
+ return;
238
+ }
239
+
65
240
  try {
66
241
  let result;
67
242
 
@@ -82,6 +257,10 @@ class McpServer {
82
257
  result = this.handleResourcesList();
83
258
  break;
84
259
 
260
+ case 'ping':
261
+ result = {};
262
+ break;
263
+
85
264
  default:
86
265
  this.sendError(-32601, `Method not found: ${method}`, id);
87
266
  return;
@@ -203,6 +382,64 @@ class McpServer {
203
382
  },
204
383
  required: ['title', 'description', 'layers']
205
384
  }
385
+ },
386
+ // CreatePopup 工具
387
+ {
388
+ name: 'CreatePopup',
389
+ description: '根据配置生成Mapbox弹框(Popup)样式配置。支持多种预设主题(浅色、深色、简约),返回完整的弹框配置对象。',
390
+ inputSchema: {
391
+ type: 'object',
392
+ properties: {
393
+ name: {
394
+ type: 'string',
395
+ description: '弹框样式名称'
396
+ },
397
+ closeButton: {
398
+ type: 'boolean',
399
+ description: '是否显示右上角的关闭按钮'
400
+ },
401
+ closeOnClick: {
402
+ type: 'boolean',
403
+ description: '点击地图任意地方时是否自动关闭Popup'
404
+ },
405
+ closeOnMove: {
406
+ type: 'boolean',
407
+ description: '地图移动(拖动或缩放)时是否自动关闭Popup'
408
+ },
409
+ anchor: {
410
+ type: 'string',
411
+ enum: ['auto', 'center', 'top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right'],
412
+ description: 'Popup箭头朝向锚点位置'
413
+ },
414
+ offset: {
415
+ type: 'array',
416
+ items: { type: 'number' },
417
+ description: '偏移量,用于微调Popup位置,格式为[x, y],单位为像素'
418
+ },
419
+ maxWidth: {
420
+ type: 'number',
421
+ description: '弹框最大宽度,单位为像素(px),范围100-800'
422
+ },
423
+ className: {
424
+ type: 'string',
425
+ description: '给Popup添加自定义CSS class名'
426
+ },
427
+ altitude: {
428
+ type: 'number',
429
+ description: '弹窗在地图表面上方的海拔高度(米),范围0-10000'
430
+ },
431
+ htmlContent: {
432
+ type: 'string',
433
+ description: '弹框的HTML内容,支持完整的HTML标签和内联样式'
434
+ },
435
+ theme: {
436
+ type: 'string',
437
+ enum: ['light', 'dark', 'minimal', 'custom'],
438
+ description: '预设主题样式: light(浅色), dark(深色), minimal(简约), custom(自定义)'
439
+ }
440
+ },
441
+ required: ['name', 'htmlContent']
442
+ }
206
443
  }
207
444
  ]
208
445
  };
@@ -239,12 +476,20 @@ class McpServer {
239
476
  async handleToolsCall(params) {
240
477
  const { name, arguments: args } = params;
241
478
 
242
- if (name !== 'CreateMap') {
243
- throw new Error(`Unknown tool: ${name}`);
244
- }
245
-
246
479
  try {
247
- const result = await this.createMap.create(args);
480
+ let result;
481
+
482
+ switch (name) {
483
+ case 'CreateMap':
484
+ result = await this.createMap.create(args);
485
+ break;
486
+ case 'CreatePopup':
487
+ result = await this.createPopup.create(args);
488
+ break;
489
+ default:
490
+ throw new Error(`Unknown tool: ${name}`);
491
+ }
492
+
248
493
  return {
249
494
  content: [
250
495
  {