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/README.md +22 -4
- package/package.json +1 -1
- package/src/config/index.js +4 -4
- package/src/index.backup.js +340 -0
- package/src/index.js +77 -200
- package/src/index.new.js +213 -0
- package/src/index.simple.js +213 -0
- package/src/infrastructure/cache.js +1 -0
- package/src/infrastructure/gracefulShutdown.js +4 -0
- package/src/infrastructure/httpClient.js +15 -5
- package/src/services/fileManager.js +61 -18
- package/src/services/linkValidator.js +15 -8
- package/src/services/orchestrator.js +79 -32
- package/src/services/orchestrator.simple.js +259 -0
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
|
-
|
|
146
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
333
|
-
|
|
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
|
-
|
|
223
|
+
console.error('[MCP] Server is running');
|
|
347
224
|
}
|
|
348
225
|
|
|
349
226
|
main().catch((error) => {
|
|
350
|
-
|
|
227
|
+
console.error(`[MCP] Startup error: ${error.message}`);
|
|
351
228
|
process.exit(1);
|
|
352
229
|
});
|
package/src/index.new.js
ADDED
|
@@ -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
|
+
});
|