htmlgen-mcp 0.2.5__py3-none-any.whl → 0.3.1__py3-none-any.whl

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.

Potentially problematic release.


This version of htmlgen-mcp might be problematic. Click here for more details.

@@ -333,6 +333,35 @@ def create_responsive_navbar(file_path: str, brand_name: str = "公司名称", n
333
333
  # 没找到 body,兜底:前置插入
334
334
  content = navbar_html + "\n" + content
335
335
 
336
+ # 确保 Bootstrap 依赖存在(导航栏需要)
337
+ bootstrap_css = '<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">'
338
+ bootstrap_js = '<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>'
339
+ fontawesome_css = '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">'
340
+
341
+ # 添加 Bootstrap CSS(如果不存在)
342
+ if 'bootstrap' not in content.lower():
343
+ if '</head>' in content:
344
+ # 在 </head> 前插入,但要在自定义 CSS 之前
345
+ if '<link rel="stylesheet" href="assets/css/style.css">' in content:
346
+ content = content.replace(
347
+ '<link rel="stylesheet" href="assets/css/style.css">',
348
+ f'{bootstrap_css}\n {fontawesome_css}\n <link rel="stylesheet" href="assets/css/style.css">'
349
+ )
350
+ else:
351
+ content = content.replace('</head>', f' {bootstrap_css}\n {fontawesome_css}\n</head>')
352
+
353
+ # 添加 Bootstrap JS(如果不存在)
354
+ if 'bootstrap.bundle' not in content.lower():
355
+ if '</body>' in content:
356
+ # 在 </body> 前插入
357
+ if '<script src="assets/js/main.js"></script>' in content:
358
+ content = content.replace(
359
+ '<script src="assets/js/main.js"></script>',
360
+ f'{bootstrap_js}\n <script src="assets/js/main.js"></script>'
361
+ )
362
+ else:
363
+ content = content.replace('</body>', f' {bootstrap_js}\n</body>')
364
+
336
365
  # 确保导航锚点对应的区块存在
337
366
  id_pattern = re.compile(r'id\s*=\s*["\']([^"\']+)["\']', re.I)
338
367
  existing_ids = {m.group(1).strip().lower() for m in id_pattern.finditer(content)}
htmlgen_mcp/config.py ADDED
@@ -0,0 +1,326 @@
1
+ """项目配置管理 - 跨平台支持"""
2
+ import os
3
+ import sys
4
+ import platform
5
+ import tempfile
6
+ from pathlib import Path
7
+ from datetime import datetime
8
+
9
+
10
+ class ProjectConfig:
11
+ """项目配置管理器 - 支持 Windows/macOS/Linux"""
12
+
13
+ @staticmethod
14
+ def get_system_info() -> dict:
15
+ """获取系统信息"""
16
+ return {
17
+ 'system': platform.system(), # 'Windows', 'Darwin' (macOS), 'Linux'
18
+ 'platform': sys.platform, # 'win32', 'darwin', 'linux'
19
+ 'home': Path.home(),
20
+ 'temp': Path(tempfile.gettempdir())
21
+ }
22
+
23
+ @staticmethod
24
+ def get_default_output_dir() -> Path:
25
+ """获取默认的项目输出目录(跨平台)
26
+
27
+ 优先级:
28
+ 1. 环境变量 WEB_AGENT_OUTPUT_DIR
29
+ 2. 系统特定的文档目录
30
+ 3. 用户主目录下的隐藏目录
31
+ 4. 系统临时目录
32
+ """
33
+
34
+ # 1. 检查环境变量(所有平台通用)
35
+ env_dir = os.environ.get('WEB_AGENT_OUTPUT_DIR')
36
+ if env_dir:
37
+ output_dir = Path(env_dir)
38
+ try:
39
+ output_dir.mkdir(parents=True, exist_ok=True)
40
+ return output_dir
41
+ except Exception as e:
42
+ print(f"⚠️ 无法创建环境变量指定的目录: {e}")
43
+
44
+ system_info = ProjectConfig.get_system_info()
45
+ home = system_info['home']
46
+ system = system_info['system']
47
+
48
+ # 2. 系统特定的文档目录
49
+ if system == 'Windows':
50
+ # Windows: 使用 Documents 文件夹
51
+ docs_candidates = [
52
+ home / 'Documents' / 'WebProjects',
53
+ home / 'My Documents' / 'WebProjects', # 旧版 Windows
54
+ Path(os.environ.get('USERPROFILE', home)) / 'Documents' / 'WebProjects'
55
+ ]
56
+ elif system == 'Darwin': # macOS
57
+ # macOS: Documents 文件夹
58
+ docs_candidates = [
59
+ home / 'Documents' / 'WebProjects',
60
+ home / 'Projects' / 'WebProjects' # 有些用户喜欢用 Projects 文件夹
61
+ ]
62
+ else: # Linux 及其他 Unix-like 系统
63
+ # Linux: 遵循 XDG 标准
64
+ xdg_documents = os.environ.get('XDG_DOCUMENTS_DIR')
65
+ docs_candidates = []
66
+ if xdg_documents:
67
+ docs_candidates.append(Path(xdg_documents) / 'WebProjects')
68
+ docs_candidates.extend([
69
+ home / 'Documents' / 'WebProjects',
70
+ home / 'projects' / 'web', # Linux 用户常用小写
71
+ home / 'Projects' / 'Web'
72
+ ])
73
+
74
+ # 尝试创建文档目录
75
+ for doc_dir in docs_candidates:
76
+ try:
77
+ if doc_dir.parent.exists():
78
+ doc_dir.mkdir(parents=True, exist_ok=True)
79
+ return doc_dir
80
+ except Exception:
81
+ continue
82
+
83
+ # 3. 用户主目录下的隐藏目录(所有平台)
84
+ if system == 'Windows':
85
+ # Windows 使用 AppData
86
+ app_data = os.environ.get('APPDATA')
87
+ if app_data:
88
+ hidden_dirs = [
89
+ Path(app_data) / 'WebAgent' / 'projects',
90
+ home / 'AppData' / 'Roaming' / 'WebAgent' / 'projects'
91
+ ]
92
+ else:
93
+ hidden_dirs = [home / '.web-agent' / 'projects']
94
+ else:
95
+ # macOS 和 Linux 使用点开头的隐藏目录
96
+ hidden_dirs = [
97
+ home / '.web-agent' / 'projects',
98
+ home / '.local' / 'share' / 'web-agent' / 'projects' # XDG 标准
99
+ ]
100
+
101
+ for hidden_dir in hidden_dirs:
102
+ try:
103
+ hidden_dir.mkdir(parents=True, exist_ok=True)
104
+ return hidden_dir
105
+ except Exception:
106
+ continue
107
+
108
+ # 4. 系统临时目录(最后的选择)
109
+ temp_base = system_info['temp']
110
+ temp_dir = temp_base / 'web-agent-projects'
111
+ try:
112
+ temp_dir.mkdir(parents=True, exist_ok=True)
113
+ print(f"⚠️ 使用临时目录: {temp_dir}")
114
+ print(f"💡 建议设置环境变量 WEB_AGENT_OUTPUT_DIR 到更合适的位置")
115
+ return temp_dir
116
+ except Exception as e:
117
+ # 如果连临时目录都无法创建,使用当前工作目录
118
+ print(f"⚠️ 无法创建临时目录: {e}")
119
+ fallback = Path.cwd() / 'web-projects'
120
+ fallback.mkdir(parents=True, exist_ok=True)
121
+ return fallback
122
+
123
+ @staticmethod
124
+ def create_project_directory(
125
+ project_name: str,
126
+ base_dir: Path = None,
127
+ use_timestamp: bool = True
128
+ ) -> Path:
129
+ """创建项目目录
130
+
131
+ Args:
132
+ project_name: 项目名称
133
+ base_dir: 基础目录,如果不提供则使用默认目录
134
+ use_timestamp: 是否在目录名中添加时间戳(避免冲突)
135
+
136
+ Returns:
137
+ 创建的项目目录路径
138
+ """
139
+ if base_dir is None:
140
+ base_dir = ProjectConfig.get_default_output_dir()
141
+
142
+ # 清理项目名称,移除特殊字符
143
+ safe_name = "".join(c for c in project_name if c.isalnum() or c in (' ', '-', '_'))
144
+ safe_name = safe_name.strip().replace(' ', '-').lower()
145
+
146
+ if use_timestamp:
147
+ # 添加时间戳避免冲突
148
+ timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
149
+ dir_name = f"{safe_name}-{timestamp}"
150
+ else:
151
+ dir_name = safe_name
152
+
153
+ project_dir = base_dir / dir_name
154
+
155
+ # 如果目录已存在且不使用时间戳,添加序号
156
+ if project_dir.exists() and not use_timestamp:
157
+ counter = 1
158
+ while (base_dir / f"{dir_name}-{counter}").exists():
159
+ counter += 1
160
+ project_dir = base_dir / f"{dir_name}-{counter}"
161
+
162
+ project_dir.mkdir(parents=True, exist_ok=True)
163
+
164
+ # 创建标准子目录结构
165
+ (project_dir / "assets" / "css").mkdir(parents=True, exist_ok=True)
166
+ (project_dir / "assets" / "js").mkdir(parents=True, exist_ok=True)
167
+ (project_dir / "assets" / "images").mkdir(parents=True, exist_ok=True)
168
+
169
+ # 创建项目信息文件
170
+ info_file = project_dir / ".project-info.json"
171
+ import json
172
+ project_info = {
173
+ "name": project_name,
174
+ "created_at": datetime.now().isoformat(),
175
+ "generator": "htmlgen-mcp",
176
+ "version": "0.3.0"
177
+ }
178
+ info_file.write_text(json.dumps(project_info, ensure_ascii=False, indent=2))
179
+
180
+ return project_dir
181
+
182
+ @staticmethod
183
+ def get_user_projects_list(base_dir: Path = None) -> list:
184
+ """获取用户已创建的项目列表
185
+
186
+ Returns:
187
+ 项目信息列表
188
+ """
189
+ if base_dir is None:
190
+ base_dir = ProjectConfig.get_default_output_dir()
191
+
192
+ projects = []
193
+ if not base_dir.exists():
194
+ return projects
195
+
196
+ for item in base_dir.iterdir():
197
+ if item.is_dir():
198
+ info_file = item / ".project-info.json"
199
+ if info_file.exists():
200
+ try:
201
+ import json
202
+ info = json.loads(info_file.read_text())
203
+ info['path'] = str(item)
204
+ projects.append(info)
205
+ except:
206
+ # 如果没有info文件,仍然添加基本信息
207
+ projects.append({
208
+ 'name': item.name,
209
+ 'path': str(item),
210
+ 'created_at': datetime.fromtimestamp(item.stat().st_mtime).isoformat()
211
+ })
212
+
213
+ # 按创建时间倒序排序
214
+ projects.sort(key=lambda x: x.get('created_at', ''), reverse=True)
215
+ return projects
216
+
217
+ @staticmethod
218
+ def clean_old_projects(
219
+ base_dir: Path = None,
220
+ days_to_keep: int = 7,
221
+ max_projects: int = 20
222
+ ) -> int:
223
+ """清理旧项目
224
+
225
+ Args:
226
+ base_dir: 基础目录
227
+ days_to_keep: 保留最近几天的项目
228
+ max_projects: 最多保留多少个项目
229
+
230
+ Returns:
231
+ 删除的项目数量
232
+ """
233
+ if base_dir is None:
234
+ base_dir = ProjectConfig.get_default_output_dir()
235
+
236
+ projects = ProjectConfig.get_user_projects_list(base_dir)
237
+ deleted = 0
238
+
239
+ # 如果项目数超过限制,删除最旧的
240
+ if len(projects) > max_projects:
241
+ for project in projects[max_projects:]:
242
+ try:
243
+ import shutil
244
+ shutil.rmtree(project['path'])
245
+ deleted += 1
246
+ except:
247
+ pass
248
+
249
+ # 删除超过指定天数的项目
250
+ from datetime import timedelta
251
+ cutoff_date = datetime.now() - timedelta(days=days_to_keep)
252
+
253
+ for project in projects:
254
+ try:
255
+ created_at = datetime.fromisoformat(project.get('created_at', ''))
256
+ if created_at < cutoff_date:
257
+ import shutil
258
+ shutil.rmtree(project['path'])
259
+ deleted += 1
260
+ except:
261
+ pass
262
+
263
+ return deleted
264
+
265
+
266
+ # 便捷函数
267
+ def get_project_directory(project_name: str = None) -> str:
268
+ """获取项目目录(供 MCP 工具使用)
269
+
270
+ Args:
271
+ project_name: 项目名称,如果不提供则生成默认名称
272
+
273
+ Returns:
274
+ 项目目录路径字符串
275
+ """
276
+ if not project_name:
277
+ project_name = f"web-project-{datetime.now().strftime('%Y%m%d')}"
278
+
279
+ config = ProjectConfig()
280
+ project_dir = config.create_project_directory(project_name, use_timestamp=True)
281
+
282
+ print(f"📁 项目将生成在: {project_dir}")
283
+ print(f"💡 提示: 可通过设置环境变量 WEB_AGENT_OUTPUT_DIR 来自定义输出目录")
284
+
285
+ return str(project_dir)
286
+
287
+
288
+ def list_recent_projects(limit: int = 10) -> list:
289
+ """列出最近的项目
290
+
291
+ Args:
292
+ limit: 返回的项目数量
293
+
294
+ Returns:
295
+ 项目列表
296
+ """
297
+ config = ProjectConfig()
298
+ projects = config.get_user_projects_list()
299
+ return projects[:limit]
300
+
301
+
302
+ def clean_temp_projects() -> int:
303
+ """清理临时项目
304
+
305
+ Returns:
306
+ 删除的项目数量
307
+ """
308
+ config = ProjectConfig()
309
+
310
+ # 如果使用的是 /tmp 目录,更积极地清理
311
+ output_dir = config.get_default_output_dir()
312
+ if str(output_dir).startswith('/tmp'):
313
+ # 临时目录只保留1天,最多10个项目
314
+ return config.clean_old_projects(days_to_keep=1, max_projects=10)
315
+ else:
316
+ # 其他目录保留7天,最多20个项目
317
+ return config.clean_old_projects(days_to_keep=7, max_projects=20)
318
+
319
+
320
+ # 导出的功能
321
+ __all__ = [
322
+ 'ProjectConfig',
323
+ 'get_project_directory',
324
+ 'list_recent_projects',
325
+ 'clean_temp_projects'
326
+ ]
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """SSE 模式优化补丁 - 解决超时问题"""
4
+
5
+ import asyncio
6
+ import json
7
+ import os
8
+ from typing import Dict, Any, List, Optional
9
+ from pathlib import Path
10
+
11
+ async def read_progress_file_async(
12
+ file_path: str,
13
+ limit: int = 20,
14
+ chunk_size: int = 8192
15
+ ) -> tuple[List[Dict[str, Any]], int]:
16
+ """异步读取进度日志文件,避免阻塞"""
17
+ events: List[Dict[str, Any]] = []
18
+ total_lines = 0
19
+
20
+ try:
21
+ # 使用异步方式读取文件
22
+ loop = asyncio.get_event_loop()
23
+
24
+ # 异步读取文件内容
25
+ def read_file():
26
+ with open(file_path, 'r', encoding='utf-8') as f:
27
+ lines = f.readlines()
28
+ return lines
29
+
30
+ lines = await loop.run_in_executor(None, read_file)
31
+ total_lines = len(lines)
32
+
33
+ # 处理最后的 limit 行
34
+ for line in lines[-limit:]:
35
+ line = line.strip()
36
+ if not line:
37
+ continue
38
+ try:
39
+ events.append(json.loads(line))
40
+ except Exception:
41
+ continue
42
+
43
+ return events, total_lines
44
+
45
+ except Exception as e:
46
+ raise Exception(f"读取进度文件失败: {str(e)}")
47
+
48
+
49
+ async def check_file_exists_async(file_path: str) -> bool:
50
+ """异步检查文件是否存在"""
51
+ loop = asyncio.get_event_loop()
52
+ return await loop.run_in_executor(None, os.path.exists, file_path)
53
+
54
+
55
+ def create_streaming_response(data: Dict[str, Any], chunk_size: int = 100) -> List[Dict[str, Any]]:
56
+ """
57
+ 将大响应分块,适用于 SSE 流式传输
58
+ 避免一次性发送大量数据导致超时
59
+ """
60
+ if 'events' in data and len(data['events']) > chunk_size:
61
+ # 分块发送事件
62
+ chunks = []
63
+ events = data['events']
64
+
65
+ for i in range(0, len(events), chunk_size):
66
+ chunk_data = data.copy()
67
+ chunk_data['events'] = events[i:i + chunk_size]
68
+ chunk_data['chunk_index'] = i // chunk_size
69
+ chunk_data['total_chunks'] = (len(events) + chunk_size - 1) // chunk_size
70
+ chunks.append(chunk_data)
71
+
72
+ return chunks
73
+
74
+ return [data]
75
+
76
+
77
+ async def get_progress_optimized(
78
+ plan_id: Optional[str] = None,
79
+ job_id: Optional[str] = None,
80
+ log_path: Optional[str] = None,
81
+ limit: int = 20,
82
+ _job_registry: Dict = None,
83
+ _progress_log_by_job: Dict = None,
84
+ _progress_log_by_id: Dict = None,
85
+ project_root: str = None
86
+ ) -> Dict[str, Any]:
87
+ """
88
+ SSE 优化版的 get_progress 函数
89
+ - 使用异步 I/O 避免阻塞
90
+ - 添加超时控制
91
+ - 支持流式响应
92
+ """
93
+ try:
94
+ # 设置操作超时(SSE 环境下更短的超时)
95
+ timeout = 10 # 10秒超时
96
+
97
+ async def _get_progress():
98
+ if limit <= 0:
99
+ limit = 20
100
+
101
+ job_info = None
102
+ resolved_path = None
103
+
104
+ # 查找日志文件路径
105
+ if job_id and _job_registry:
106
+ job_info = _job_registry.get(job_id)
107
+ if job_info and not plan_id:
108
+ plan_id = job_info.get("plan_id")
109
+ if _progress_log_by_job and job_id in _progress_log_by_job:
110
+ resolved_path = _progress_log_by_job[job_id]
111
+ elif job_info and job_info.get("progress_log"):
112
+ resolved_path = job_info.get("progress_log")
113
+
114
+ if not resolved_path and plan_id and _progress_log_by_id and plan_id in _progress_log_by_id:
115
+ resolved_path = _progress_log_by_id[plan_id]
116
+
117
+ if log_path:
118
+ resolved_path = log_path
119
+
120
+ # 解析路径
121
+ if resolved_path:
122
+ if not os.path.isabs(resolved_path):
123
+ candidate = os.path.join(project_root or os.getcwd(), resolved_path)
124
+ if await check_file_exists_async(candidate):
125
+ resolved_path = candidate
126
+ else:
127
+ alt = os.path.sep + resolved_path.lstrip(os.path.sep)
128
+ if await check_file_exists_async(alt):
129
+ resolved_path = alt
130
+
131
+ # 检查文件是否存在
132
+ if not resolved_path or not await check_file_exists_async(resolved_path):
133
+ return {
134
+ "status": "error",
135
+ "message": "未找到进度日志,请确认 job_id/plan_id 或提供 log_path"
136
+ }
137
+
138
+ # 异步读取文件
139
+ events, total_lines = await read_progress_file_async(resolved_path, limit)
140
+
141
+ response = {
142
+ "status": "success",
143
+ "plan_id": plan_id,
144
+ "job_id": job_id,
145
+ "log_path": resolved_path,
146
+ "events": events,
147
+ "total_records": total_lines,
148
+ "returned": len(events),
149
+ }
150
+
151
+ # 添加任务信息
152
+ if job_info:
153
+ snapshot_keys = [
154
+ "job_id", "status", "plan_id", "progress_log",
155
+ "started_at", "updated_at", "completed_at",
156
+ "project_directory", "model", "upload_status",
157
+ "website_url", "upload_completed_at"
158
+ ]
159
+ job_snapshot = {
160
+ k: job_info.get(k) for k in snapshot_keys
161
+ if job_info.get(k) is not None
162
+ }
163
+
164
+ if job_info.get("status") == "completed":
165
+ job_snapshot["result_summary"] = {
166
+ "report": job_info.get("result", {}).get("report"),
167
+ "created_files": job_info.get("result", {}).get("created_files"),
168
+ }
169
+ if job_info.get("upload_result"):
170
+ job_snapshot["upload_result"] = job_info.get("upload_result")
171
+
172
+ if job_info.get("status") == "failed":
173
+ job_snapshot["error"] = job_info.get("error")
174
+
175
+ if job_info.get("upload_error"):
176
+ job_snapshot["upload_error"] = job_info.get("upload_error")
177
+
178
+ response["job"] = job_snapshot
179
+
180
+ return response
181
+
182
+ # 使用超时控制
183
+ result = await asyncio.wait_for(_get_progress(), timeout=timeout)
184
+ return result
185
+
186
+ except asyncio.TimeoutError:
187
+ return {
188
+ "status": "error",
189
+ "message": f"操作超时({timeout}秒)- SSE 模式下请减少 limit 参数值"
190
+ }
191
+ except Exception as exc:
192
+ return {
193
+ "status": "error",
194
+ "message": str(exc)
195
+ }
@@ -912,6 +912,9 @@ async def get_progress(
912
912
  """
913
913
 
914
914
  try:
915
+ # SSE 模式优化:使用异步 I/O
916
+ loop = asyncio.get_event_loop()
917
+
915
918
  if limit <= 0:
916
919
  limit = 20
917
920
 
@@ -936,32 +939,48 @@ async def get_progress(
936
939
  if resolved_path:
937
940
  if not os.path.isabs(resolved_path):
938
941
  candidate = os.path.join(PROJECT_ROOT, resolved_path)
939
- if os.path.exists(candidate):
942
+ # 异步检查文件存在
943
+ exists = await loop.run_in_executor(None, os.path.exists, candidate)
944
+ if exists:
940
945
  resolved_path = candidate
941
946
  else:
942
947
  alt = os.path.sep + resolved_path.lstrip(os.path.sep)
943
- if os.path.exists(alt):
948
+ exists_alt = await loop.run_in_executor(None, os.path.exists, alt)
949
+ if exists_alt:
944
950
  resolved_path = alt
945
951
 
946
- if not resolved_path or not os.path.exists(resolved_path):
952
+ # 异步检查最终路径
953
+ path_exists = await loop.run_in_executor(
954
+ None, lambda: resolved_path and os.path.exists(resolved_path)
955
+ )
956
+
957
+ if not path_exists:
947
958
  return {
948
959
  "status": "error",
949
960
  "message": "未找到进度日志,请确认 job_id/plan_id 或提供 log_path(注意绝对路径需以/开头,扩展名为 .jsonl)",
950
961
  }
951
962
 
952
- events: list[Dict[str, Any]] = []
953
- total_lines = 0
954
- with open(resolved_path, "r", encoding="utf-8") as f:
955
- lines = f.readlines()
956
- total_lines = len(lines)
957
- for line in lines[-limit:]:
958
- line = line.strip()
959
- if not line:
960
- continue
961
- try:
962
- events.append(json.loads(line))
963
- except Exception:
964
- continue
963
+ # 异步读取文件
964
+ def read_file():
965
+ events = []
966
+ total = 0
967
+ try:
968
+ with open(resolved_path, "r", encoding="utf-8") as f:
969
+ lines = f.readlines()
970
+ total = len(lines)
971
+ for line in lines[-limit:]:
972
+ line = line.strip()
973
+ if not line:
974
+ continue
975
+ try:
976
+ events.append(json.loads(line))
977
+ except Exception:
978
+ continue
979
+ except Exception:
980
+ pass
981
+ return events, total
982
+
983
+ events, total_lines = await loop.run_in_executor(None, read_file)
965
984
 
966
985
  response: Dict[str, Any] = {
967
986
  "status": "success",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: htmlgen-mcp
3
- Version: 0.2.5
3
+ Version: 0.3.1
4
4
  Summary: AI-powered HTML website generator with auto-upload functionality via Model Context Protocol
5
5
  Author-email: HTML Generator Team <contact@htmlgen-mcp.com>
6
6
  License: MIT