smart-image-scraper-mcp 2.5.2 → 2.7.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.
package/src/index.js CHANGED
@@ -4,13 +4,10 @@
4
4
  * 全网智能图片抓取 MCP 服务器
5
5
  * 基于 Model Context Protocol 的图片搜索、验证、下载工具
6
6
  *
7
- * 生产级功能:
8
- * - 优雅关闭和资源清理
9
- * - 健康检查和状态监控
10
- * - 性能指标收集
11
- * - 速率限制
12
- * - 缓存机制
13
- * - 错误分类
7
+ * 设计原则(模仿主流 MCP 实现):
8
+ * - 简洁:最小化基础设施代码
9
+ * - 无状态:每个请求独立处理
10
+ * - 可靠:简单的错误处理,避免资源泄漏
14
11
  */
15
12
 
16
13
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
@@ -22,16 +19,8 @@ import {
22
19
  import { createRequire } from 'module';
23
20
 
24
21
  import { Orchestrator } from './services/orchestrator.js';
25
- import logger from './infrastructure/logger.js';
26
- import { gracefulShutdown } from './infrastructure/gracefulShutdown.js';
27
- import { healthChecker } from './infrastructure/healthCheck.js';
28
- import { metrics } from './infrastructure/metrics.js';
29
- import { searchCache, validationCache } from './infrastructure/cache.js';
30
22
  import config from './config/index.js';
31
23
 
32
- // 请求计数器,用于定期清理
33
- let requestCount = 0;
34
-
35
24
  // 从 package.json 读取版本号
36
25
  const require = createRequire(import.meta.url);
37
26
  const packageJson = require('../package.json');
@@ -40,7 +29,7 @@ const packageJson = require('../package.json');
40
29
  const server = new Server(
41
30
  {
42
31
  name: 'smart-image-scraper',
43
- version: packageJson.version, // 动态读取版本号
32
+ version: packageJson.version,
44
33
  },
45
34
  {
46
35
  capabilities: {
@@ -49,9 +38,6 @@ const server = new Server(
49
38
  }
50
39
  );
51
40
 
52
- // 创建编排器实例
53
- const orchestrator = new Orchestrator();
54
-
55
41
  // 定义 Tool Schema
56
42
  const SMART_SCRAPER_TOOL = {
57
43
  name: 'smart_scraper',
@@ -66,7 +52,8 @@ const SMART_SCRAPER_TOOL = {
66
52
  【参数选择指南】
67
53
  - 用户要"找/搜索/查找图片" → mode="link"
68
54
  - 用户要"下载/保存/获取图片" → mode="download"
69
- - 用户要"高清/大图/壁纸" → size="large" 或 "wallpaper"
55
+ - 用户要"高清/大图/壁纸" → size="large" 或 "wallpaper",quality="high"
56
+ - 用户要"高质量/精选/优质" → quality="high"
70
57
  - 用户要"电脑壁纸/横屏/横向" → aspect="wide"
71
58
  - 用户要"手机壁纸/竖屏/竖向" → aspect="tall"
72
59
  - 用户要"统一尺寸/固定大小" → targetSize="1920x1080" 或预设名
@@ -83,7 +70,8 @@ const SMART_SCRAPER_TOOL = {
83
70
  2. 下载10张高清风景图: {"query":"风景","mode":"download","count":10,"size":"large"}
84
71
  3. 下载电脑壁纸并统一为1080p: {"query":"风景","mode":"download","count":10,"aspect":"wide","targetSize":"desktop_1080p"}
85
72
  4. 下载手机壁纸: {"query":"动漫","mode":"download","count":10,"aspect":"tall","targetSize":"mobile_hd"}
86
- 5. 批量下载多类图片: {"query":"猫,狗,兔子","mode":"download","count":5}`,
73
+ 5. 批量下载多类图片: {"query":"猫,狗,兔子","mode":"download","count":5}
74
+ 6. 获取高质量图片: {"query":"风景","mode":"link","count":5,"size":"large","quality":"high"}`,
87
75
  inputSchema: {
88
76
  type: 'object',
89
77
  properties: {
@@ -135,218 +123,107 @@ const SMART_SCRAPER_TOOL = {
135
123
  description: '安全搜索。off=关闭;moderate=中等过滤(默认);strict=严格过滤(儿童/家庭内容)',
136
124
  default: 'moderate',
137
125
  },
126
+ quality: {
127
+ type: 'string',
128
+ enum: ['fast', 'balanced', 'high'],
129
+ description: '质量模式。fast=快速返回(不验证,速度最快);balanced=平衡模式(验证有效性,默认);high=高质量优先(验证+按质量排序,速度较慢但质量最好)',
130
+ default: 'balanced',
131
+ },
132
+ minFileSize: {
133
+ type: 'string',
134
+ enum: ['any', '50kb', '100kb', '200kb', '500kb', '1mb'],
135
+ description: '最小文件大小过滤。文件越大通常质量越高。any=不限制;建议高清图片用100kb以上',
136
+ default: 'any',
137
+ },
138
138
  },
139
139
  required: ['query', 'mode'],
140
140
  },
141
141
  };
142
142
 
143
- // 注册工具列表处理器
144
- server.setRequestHandler(ListToolsRequestSchema, async () => {
145
- return {
146
- tools: [SMART_SCRAPER_TOOL],
147
- };
148
- });
143
+ // 注册工具列表(主流做法:简单返回)
144
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
145
+ tools: [SMART_SCRAPER_TOOL],
146
+ }));
149
147
 
150
- // 注册工具调用处理器
148
+ // 注册工具调用(主流做法:每个请求创建新实例,避免状态污染)
151
149
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
152
- const requestStartTime = Date.now();
153
150
  const { name, arguments: args } = request.params;
154
-
155
- logger.info(`[MCP] 收到请求: ${name}, query="${args?.query?.substring(0, 30)}..."`);
156
151
 
157
152
  if (name !== 'smart_scraper') {
158
153
  return {
159
- content: [
160
- {
161
- type: 'text',
162
- text: `未知工具: ${name}`,
163
- },
164
- ],
154
+ content: [{ type: 'text', text: `未知工具: ${name}` }],
165
155
  isError: true,
166
156
  };
167
157
  }
168
158
 
169
- try {
170
- // 参数校验
171
- if (!args.query || typeof args.query !== 'string') {
172
- return {
173
- content: [
174
- {
175
- type: 'text',
176
- text: '错误: 请提供有效的搜索关键词 (query)',
177
- },
178
- ],
179
- isError: true,
180
- };
181
- }
182
-
183
- if (!args.mode || !['link', 'download'].includes(args.mode)) {
184
- return {
185
- content: [
186
- {
187
- type: 'text',
188
- text: "错误: 请指定有效的运行模式 (mode): 'link' 或 'download'",
189
- },
190
- ],
191
- isError: true,
192
- };
193
- }
194
-
195
- // 验证 count 参数
196
- let count = parseInt(args.count, 10) || 10;
197
- if (count < 1) count = 1;
198
- if (count > 100) count = 100; // 限制最大数量
199
-
200
- // 验证 query 长度
201
- const query = args.query.trim();
202
- if (query.length === 0) {
203
- return {
204
- content: [
205
- {
206
- type: 'text',
207
- text: '错误: 搜索关键词不能为空',
208
- },
209
- ],
210
- isError: true,
211
- };
212
- }
213
- if (query.length > 500) {
214
- return {
215
- content: [
216
- {
217
- type: 'text',
218
- text: '错误: 搜索关键词过长(最大500字符)',
219
- },
220
- ],
221
- isError: true,
222
- };
223
- }
224
-
225
- // 验证 source 参数
226
- const source = args.source || 'bing';
227
- if (!['bing', 'google'].includes(source)) {
228
- return {
229
- content: [
230
- {
231
- type: 'text',
232
- text: "错误: 无效的搜索源,请使用 'bing' 或 'google'",
233
- },
234
- ],
235
- isError: true,
236
- };
237
- }
238
-
239
- // 检查是否正在关闭
240
- if (gracefulShutdown.isShuttingDownNow()) {
241
- return {
242
- content: [{ type: 'text', text: '服务器正在关闭,无法处理新请求' }],
243
- isError: true,
244
- };
245
- }
246
-
247
- // 开始操作追踪
248
- const operation = gracefulShutdown.startOperation(`scraper:${query.substring(0, 20)}`);
249
- const startTime = Date.now();
250
-
251
- try {
252
- // 执行任务(requestQueue 已有超时机制,这里不再重复设置)
253
- logger.info('Executing smart_scraper', { args });
254
-
255
- const result = await orchestrator.execute({
256
- query: query,
257
- mode: args.mode,
258
- count: count,
259
- source: source,
260
- size: args.size || 'all',
261
- aspect: args.aspect || 'all',
262
- targetSize: args.targetSize || null,
263
- fit: args.fit || 'cover',
264
- safeSearch: args.safeSearch || 'moderate',
265
- });
266
-
267
- // 记录成功指标
268
- metrics.recordSearch(source, true, Date.now() - startTime, result.results?.length || 0);
269
-
270
- // 格式化输出
271
- const formattedResult = orchestrator.formatResult(result);
272
-
273
- const totalTime = Date.now() - requestStartTime;
274
- logger.info(`[MCP] 请求完成: ${totalTime}ms, requestId=${result.requestId}`);
275
-
276
- // 每3个请求清理一次缓存,避免内存累积
277
- requestCount++;
278
- if (requestCount >= 3) {
279
- searchCache.clear();
280
- validationCache.clear();
281
- requestCount = 0;
282
- logger.info('[MCP] 缓存已清理');
283
- }
159
+ // 参数验证(主流做法:快速失败)
160
+ if (!args?.query || typeof args.query !== 'string' || !args.query.trim()) {
161
+ return {
162
+ content: [{ type: 'text', text: '错误: 请提供有效的搜索关键词 (query)' }],
163
+ isError: true,
164
+ };
165
+ }
284
166
 
285
- const response = {
286
- content: [
287
- {
288
- type: 'text',
289
- text: formattedResult,
290
- },
291
- ],
292
- };
293
-
294
- return response;
295
- } catch (innerError) {
296
- // 记录失败指标
297
- metrics.recordSearch(source, false, Date.now() - startTime);
298
- metrics.recordError(innerError);
299
- logger.error(`[MCP] 内部错误: ${innerError.message}`);
300
- throw innerError;
301
- } finally {
302
- // 结束操作追踪
303
- operation.end();
304
- }
305
- } catch (error) {
306
- const totalTime = Date.now() - requestStartTime;
307
- logger.error(`[MCP] 请求失败: ${totalTime}ms, error=${error.message}`);
167
+ if (!args.mode || !['link', 'download'].includes(args.mode)) {
308
168
  return {
309
- content: [
310
- {
311
- type: 'text',
312
- text: `## ❌ 执行错误\n\n**错误信息**: ${error.message}\n\n请检查网络连接或稍后重试。`,
313
- },
314
- ],
169
+ content: [{ type: 'text', text: "错误: 请指定有效的运行模式 (mode): 'link' 或 'download'" }],
315
170
  isError: true,
316
171
  };
317
172
  }
318
- });
319
173
 
320
- // 注册关闭回调
321
- gracefulShutdown.onShutdown(async () => {
322
- logger.info('Closing MCP server connection...');
323
174
  try {
324
- await server.close();
175
+ // 主流做法:每个请求创建新的 Orchestrator 实例,确保无状态
176
+ const orchestrator = new Orchestrator();
177
+
178
+ // 规范化参数
179
+ const params = {
180
+ query: args.query.trim(),
181
+ mode: args.mode,
182
+ count: Math.min(Math.max(parseInt(args.count, 10) || 10, 1), 100),
183
+ source: ['bing', 'google'].includes(args.source) ? args.source : 'bing',
184
+ size: args.size || 'all',
185
+ aspect: args.aspect || 'all',
186
+ targetSize: args.targetSize || null,
187
+ fit: args.fit || 'cover',
188
+ safeSearch: args.safeSearch || 'moderate',
189
+ quality: ['fast', 'balanced', 'high'].includes(args.quality) ? args.quality : 'balanced',
190
+ minFileSize: ['any', '50kb', '100kb', '200kb', '500kb', '1mb'].includes(args.minFileSize) ? args.minFileSize : 'any',
191
+ };
192
+
193
+ // 执行任务
194
+ const result = await orchestrator.execute(params);
195
+
196
+ // 格式化输出
197
+ const formattedResult = orchestrator.formatResult(result);
198
+
199
+ return {
200
+ content: [{ type: 'text', text: formattedResult }],
201
+ };
325
202
  } catch (error) {
326
- logger.warn('Error closing server', { error: error.message });
203
+ // 主流做法:简洁的错误处理,使用 stderr 输出日志
204
+ console.error(`[MCP Error] ${error.message}`);
205
+ return {
206
+ content: [{
207
+ type: 'text',
208
+ text: `## ❌ 执行错误\n\n**错误信息**: ${error.message}\n\n请检查网络连接或稍后重试。`
209
+ }],
210
+ isError: true,
211
+ };
327
212
  }
328
213
  });
329
214
 
330
- // 启动服务器
215
+ // 启动服务器(主流做法:最简启动,使用 stderr 输出日志避免干扰 stdio 通信)
331
216
  async function main() {
332
- logger.info('Starting Smart Image Scraper MCP Server...');
333
- logger.info('Configuration', {
334
- saveRoot: config.SAVE_ROOT,
335
- maxKeywordConcurrency: config.MAX_KEYWORD_CONCURRENCY,
336
- maxDownloadConcurrency: config.MAX_DOWNLOAD_CONCURRENCY,
337
- });
338
-
339
- // 执行初始健康检查
340
- const healthResult = await healthChecker.runAllChecks();
341
- logger.info('Initial health check', { status: healthResult.status });
217
+ console.error(`[MCP] Starting Smart Image Scraper v${packageJson.version}`);
218
+ console.error(`[MCP] Save root: ${config.SAVE_ROOT}`);
342
219
 
343
220
  const transport = new StdioServerTransport();
344
221
  await server.connect(transport);
345
222
 
346
- logger.info('MCP Server is running and ready to accept requests');
223
+ console.error('[MCP] Server is running');
347
224
  }
348
225
 
349
226
  main().catch((error) => {
350
- logger.error('Server startup error', { error: error.message });
227
+ console.error(`[MCP] Startup error: ${error.message}`);
351
228
  process.exit(1);
352
229
  });
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * 全网智能图片抓取 MCP 服务器
5
+ * 基于 Model Context Protocol 的图片搜索、验证、下载工具
6
+ *
7
+ * 设计原则(模仿主流 MCP 实现):
8
+ * - 简洁:最小化基础设施代码
9
+ * - 无状态:每个请求独立处理
10
+ * - 可靠:简单的错误处理,避免资源泄漏
11
+ */
12
+
13
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
14
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
15
+ import {
16
+ CallToolRequestSchema,
17
+ ListToolsRequestSchema,
18
+ } from '@modelcontextprotocol/sdk/types.js';
19
+ import { createRequire } from 'module';
20
+
21
+ import { Orchestrator } from './services/orchestrator.js';
22
+ import config from './config/index.js';
23
+
24
+ // 从 package.json 读取版本号
25
+ const require = createRequire(import.meta.url);
26
+ const packageJson = require('../package.json');
27
+
28
+ // 创建 MCP 服务器
29
+ const server = new Server(
30
+ {
31
+ name: 'smart-image-scraper',
32
+ version: packageJson.version,
33
+ },
34
+ {
35
+ capabilities: {
36
+ tools: {},
37
+ },
38
+ }
39
+ );
40
+
41
+ // 定义 Tool Schema
42
+ const SMART_SCRAPER_TOOL = {
43
+ name: 'smart_scraper',
44
+ description: `全网智能图片抓取工具 - 从 Bing/Google 搜索、验证、下载高质量图片。
45
+
46
+ 【核心功能】
47
+ 1. 搜索图片链接 (mode=link) - 返回验证过的图片URL列表
48
+ 2. 下载图片 (mode=download) - 下载到本地,自动按质量排序优先高清
49
+ 3. 尺寸统一 (targetSize) - 下载后自动裁剪/缩放到指定尺寸
50
+ 4. 宽高比过滤 (aspect) - 横向/竖向/正方形
51
+
52
+ 【参数选择指南】
53
+ - 用户要"找/搜索/查找图片" → mode="link"
54
+ - 用户要"下载/保存/获取图片" → mode="download"
55
+ - 用户要"高清/大图/壁纸" → size="large" 或 "wallpaper"
56
+ - 用户要"电脑壁纸/横屏/横向" → aspect="wide"
57
+ - 用户要"手机壁纸/竖屏/竖向" → aspect="tall"
58
+ - 用户要"统一尺寸/固定大小" → targetSize="1920x1080" 或预设名
59
+ - 用户要"多种类型图片" → query="猫,狗,鸟"(英文逗号分隔)
60
+
61
+ 【预设尺寸名称】
62
+ - 电脑壁纸: desktop_1080p(1920x1080), desktop_2k(2560x1440), desktop_4k(3840x2160)
63
+ - 手机壁纸: mobile_hd(1080x1920), mobile_2k(1440x2560)
64
+ - 正方形: square_1080(1080x1080), square_512(512x512)
65
+ - 社交媒体: instagram(1080x1080), twitter(1200x675), facebook(1200x630)
66
+
67
+ 【调用示例】
68
+ 1. 搜索5张猫的图片: {"query":"可爱的猫","mode":"link","count":5}
69
+ 2. 下载10张高清风景图: {"query":"风景","mode":"download","count":10,"size":"large"}
70
+ 3. 下载电脑壁纸并统一为1080p: {"query":"风景","mode":"download","count":10,"aspect":"wide","targetSize":"desktop_1080p"}
71
+ 4. 下载手机壁纸: {"query":"动漫","mode":"download","count":10,"aspect":"tall","targetSize":"mobile_hd"}
72
+ 5. 批量下载多类图片: {"query":"猫,狗,兔子","mode":"download","count":5}`,
73
+ inputSchema: {
74
+ type: 'object',
75
+ properties: {
76
+ query: {
77
+ type: 'string',
78
+ description: '搜索关键词。批量搜索用英文逗号分隔,如 "猫,狗,鸟"。建议使用具体描述性词语如"可爱的橘猫"而非"猫"',
79
+ },
80
+ mode: {
81
+ type: 'string',
82
+ enum: ['link', 'download'],
83
+ description: "运行模式。link=仅返回验证过的图片URL列表(用户只需要链接时使用);download=下载图片到本地文件系统(用户说下载/保存时使用)",
84
+ },
85
+ count: {
86
+ type: 'number',
87
+ description: '每个关键词获取的图片数量。范围1-100,推荐1-20。用户说"几张"用5-10,说"很多"用20-30',
88
+ default: 10,
89
+ },
90
+ source: {
91
+ type: 'string',
92
+ enum: ['bing', 'google'],
93
+ description: '搜索引擎。bing更稳定推荐优先使用,google结果可能更丰富但可能被限制',
94
+ default: 'bing',
95
+ },
96
+ size: {
97
+ type: 'string',
98
+ enum: ['all', 'small', 'medium', 'large', 'wallpaper'],
99
+ description: '图片尺寸。all=不限;small=小图/图标;medium=中图;large=大图/高清;wallpaper=壁纸级别(1080p+)',
100
+ default: 'all',
101
+ },
102
+ aspect: {
103
+ type: 'string',
104
+ enum: ['all', 'wide', 'tall', 'square'],
105
+ description: '图片宽高比。all=不限;wide=横向/宽屏(电脑壁纸);tall=纵向/竖屏(手机壁纸);square=正方形',
106
+ default: 'all',
107
+ },
108
+ targetSize: {
109
+ type: 'string',
110
+ description: '目标尺寸,下载后统一裁剪/缩放到此尺寸。格式: "宽x高"(如"1920x1080")或预设名(desktop_1080p/desktop_2k/desktop_4k/mobile_hd/mobile_2k/square_1080/instagram/twitter/facebook)',
111
+ },
112
+ fit: {
113
+ type: 'string',
114
+ enum: ['cover', 'contain', 'fill'],
115
+ description: '尺寸处理时的适应模式。cover=裁剪填充(默认,不留白);contain=包含留白;fill=拉伸填充',
116
+ default: 'cover',
117
+ },
118
+ safeSearch: {
119
+ type: 'string',
120
+ enum: ['off', 'moderate', 'strict'],
121
+ description: '安全搜索。off=关闭;moderate=中等过滤(默认);strict=严格过滤(儿童/家庭内容)',
122
+ default: 'moderate',
123
+ },
124
+ },
125
+ required: ['query', 'mode'],
126
+ },
127
+ };
128
+
129
+ // 注册工具列表(主流做法:简单返回)
130
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
131
+ tools: [SMART_SCRAPER_TOOL],
132
+ }));
133
+
134
+ // 注册工具调用(主流做法:每个请求创建新实例,避免状态污染)
135
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
136
+ const { name, arguments: args } = request.params;
137
+
138
+ if (name !== 'smart_scraper') {
139
+ return {
140
+ content: [{ type: 'text', text: `未知工具: ${name}` }],
141
+ isError: true,
142
+ };
143
+ }
144
+
145
+ // 参数验证(主流做法:快速失败)
146
+ if (!args?.query || typeof args.query !== 'string' || !args.query.trim()) {
147
+ return {
148
+ content: [{ type: 'text', text: '错误: 请提供有效的搜索关键词 (query)' }],
149
+ isError: true,
150
+ };
151
+ }
152
+
153
+ if (!args.mode || !['link', 'download'].includes(args.mode)) {
154
+ return {
155
+ content: [{ type: 'text', text: "错误: 请指定有效的运行模式 (mode): 'link' 或 'download'" }],
156
+ isError: true,
157
+ };
158
+ }
159
+
160
+ try {
161
+ // 主流做法:每个请求创建新的 Orchestrator 实例,确保无状态
162
+ const orchestrator = new Orchestrator();
163
+
164
+ // 规范化参数
165
+ const params = {
166
+ query: args.query.trim(),
167
+ mode: args.mode,
168
+ count: Math.min(Math.max(parseInt(args.count, 10) || 10, 1), 100),
169
+ source: ['bing', 'google'].includes(args.source) ? args.source : 'bing',
170
+ size: args.size || 'all',
171
+ aspect: args.aspect || 'all',
172
+ targetSize: args.targetSize || null,
173
+ fit: args.fit || 'cover',
174
+ safeSearch: args.safeSearch || 'moderate',
175
+ };
176
+
177
+ // 执行任务
178
+ const result = await orchestrator.execute(params);
179
+
180
+ // 格式化输出
181
+ const formattedResult = orchestrator.formatResult(result);
182
+
183
+ return {
184
+ content: [{ type: 'text', text: formattedResult }],
185
+ };
186
+ } catch (error) {
187
+ // 主流做法:简洁的错误处理,使用 stderr 输出日志
188
+ console.error(`[MCP Error] ${error.message}`);
189
+ return {
190
+ content: [{
191
+ type: 'text',
192
+ text: `## ❌ 执行错误\n\n**错误信息**: ${error.message}\n\n请检查网络连接或稍后重试。`
193
+ }],
194
+ isError: true,
195
+ };
196
+ }
197
+ });
198
+
199
+ // 启动服务器(主流做法:最简启动,使用 stderr 输出日志避免干扰 stdio 通信)
200
+ async function main() {
201
+ console.error(`[MCP] Starting Smart Image Scraper v${packageJson.version}`);
202
+ console.error(`[MCP] Save root: ${config.SAVE_ROOT}`);
203
+
204
+ const transport = new StdioServerTransport();
205
+ await server.connect(transport);
206
+
207
+ console.error('[MCP] Server is running');
208
+ }
209
+
210
+ main().catch((error) => {
211
+ console.error(`[MCP] Startup error: ${error.message}`);
212
+ process.exit(1);
213
+ });