@lmcc-dev/mult-fetch-mcp-server 1.2.1 → 1.3.1
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 +80 -29
- package/README.zh.md +80 -29
- package/dist/i18n-test-report.json +2 -2
- package/dist/i18n-unused-keys-report.json +4 -4
- package/dist/src/client.js +522 -131
- package/dist/src/index.js +0 -0
- package/dist/src/lib/fetch.js +8 -0
- package/dist/src/lib/fetchers/browser/BrowserFetcher.js +139 -199
- package/dist/src/lib/fetchers/common/BaseFetcher.js +203 -0
- package/dist/src/lib/fetchers/common/types.js +4 -0
- package/dist/src/lib/fetchers/common/utils.js +1 -1
- package/dist/src/lib/fetchers/index.js +11 -0
- package/dist/src/lib/fetchers/node/NodeFetcher.js +91 -66
- package/dist/src/lib/i18n/keys/browser.js +40 -2
- package/dist/src/lib/i18n/keys/chunkManager.js +43 -0
- package/dist/src/lib/i18n/keys/client.js +53 -1
- package/dist/src/lib/i18n/keys/contentSize.js +20 -0
- package/dist/src/lib/i18n/keys/fetcher.js +88 -79
- package/dist/src/lib/i18n/keys/index.js +4 -0
- package/dist/src/lib/i18n/keys/node.js +7 -1
- package/dist/src/lib/i18n/keys/processor.js +30 -0
- package/dist/src/lib/i18n/keys/tools.js +5 -1
- package/dist/src/lib/i18n/keys/url.js +45 -0
- package/dist/src/lib/i18n/keys.js +25 -0
- package/dist/src/lib/i18n/locales/en/browser.js +52 -14
- package/dist/src/lib/i18n/locales/en/chunkManager.js +41 -0
- package/dist/src/lib/i18n/locales/en/client.js +62 -9
- package/dist/src/lib/i18n/locales/en/contentSize.js +17 -0
- package/dist/src/lib/i18n/locales/en/fetcher.js +50 -30
- package/dist/src/lib/i18n/locales/en/index.js +9 -1
- package/dist/src/lib/i18n/locales/en/node.js +26 -20
- package/dist/src/lib/i18n/locales/en/processor.js +28 -0
- package/dist/src/lib/i18n/locales/en/tools.js +5 -1
- package/dist/src/lib/i18n/locales/en/url.js +40 -0
- package/dist/src/lib/i18n/locales/en.js +51 -0
- package/dist/src/lib/i18n/locales/zh/browser.js +85 -47
- package/dist/src/lib/i18n/locales/zh/chunkManager.js +41 -0
- package/dist/src/lib/i18n/locales/zh/client.js +53 -1
- package/dist/src/lib/i18n/locales/zh/contentSize.js +17 -0
- package/dist/src/lib/i18n/locales/zh/fetcher.js +67 -47
- package/dist/src/lib/i18n/locales/zh/index.js +9 -1
- package/dist/src/lib/i18n/locales/zh/node.js +50 -44
- package/dist/src/lib/i18n/locales/zh/processor.js +28 -0
- package/dist/src/lib/i18n/locales/zh/tools.js +5 -1
- package/dist/src/lib/i18n/locales/zh/url.js +40 -0
- package/dist/src/lib/i18n/locales/zh.js +51 -0
- package/dist/src/lib/logger.js +11 -2
- package/dist/src/lib/server/fetcher.js +15 -5
- package/dist/src/lib/server/tools.js +202 -52
- package/dist/src/lib/types.js +5 -0
- package/dist/src/lib/utils/ChunkManager.js +177 -0
- package/dist/src/lib/utils/ContentProcessor.js +134 -0
- package/dist/src/lib/utils/ContentSizeManager.js +123 -0
- package/dist/src/lib/utils/TemplateUtils.js +71 -0
- package/dist/src/lib/utils/errors.js +2 -8
- package/dist/src/mcp-server.js +0 -0
- package/dist/src/test-i18n.js +0 -0
- package/dist/tests/BrowserFetcher.test.js +0 -0
- package/dist/tests/NodeFetcher.test.js +0 -0
- package/dist/tests/client.test.js +0 -0
- package/dist/tests/fetch.test.js +0 -0
- package/dist/tests/fetchers/plain-text.test.js +146 -0
- package/dist/tests/i18n-missing-keys.js +0 -0
- package/dist/tests/i18n-remove-unused-keys.js +0 -0
- package/dist/tests/i18n-unused-keys.js +0 -0
- package/dist/tests/i18n.test.js +0 -0
- package/dist/tests/logger.test.js +0 -0
- package/dist/tests/mcp-server.test.js +0 -0
- package/dist/tests/server/fetcher.test.js +69 -15
- package/dist/tests/server/tools.test.js +165 -16
- package/dist/tests/setup.js +0 -0
- package/dist/tests/test-direct-client.js +113 -5
- package/dist/tests/test-i18n.js +0 -0
- package/dist/tests/test-mcp-methods.js +67 -1
- package/dist/tests/test-mcp.js +83 -3
- package/dist/tests/test-mini4k.js +90 -3
- package/dist/tests/types.test.js +0 -0
- package/dist/tests/utils/ChunkManager.test.js +163 -0
- package/dist/tests/utils/ContentSizeManager.test.js +170 -0
- package/dist/vitest.config.js +0 -0
- package/package.json +28 -23
|
@@ -7,6 +7,8 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
|
|
|
7
7
|
import { log, COMPONENTS } from '../logger.js';
|
|
8
8
|
import { fetchWithAutoDetect } from './fetcher.js';
|
|
9
9
|
import { closeBrowserInstance } from './browser.js';
|
|
10
|
+
import { BaseFetcher } from '../fetchers/common/BaseFetcher.js';
|
|
11
|
+
import { TemplateUtils } from '../utils/TemplateUtils.js';
|
|
10
12
|
/**
|
|
11
13
|
* 获取HTML获取工具定义 (Get HTML fetch tool definition)
|
|
12
14
|
* @returns HTML获取工具定义 (HTML fetch tool definition)
|
|
@@ -14,7 +16,7 @@ import { closeBrowserInstance } from './browser.js';
|
|
|
14
16
|
function getHtmlFetchToolDefinition() {
|
|
15
17
|
return {
|
|
16
18
|
name: "fetch_html",
|
|
17
|
-
description: "Fetch a website and return the content as HTML",
|
|
19
|
+
description: "Fetch a website and return the content as HTML. Best practices: 1) Always set startCursor=0 for initial requests, and use the fetchedBytes value from previous response for subsequent requests to ensure content continuity. 2) Set contentSizeLimit between 20000-50000 for large pages. 3) When handling large content, use the chunking system by following the startCursor instructions in the system notes rather than increasing contentSizeLimit. 4) If content retrieval fails, you can retry using the same chunkId and startCursor, or adjust startCursor as needed but you must handle any resulting data duplication or gaps yourself. 5) Always explain to users when content is chunked and ask if they want to continue retrieving subsequent parts.",
|
|
18
20
|
inputSchema: {
|
|
19
21
|
type: "object",
|
|
20
22
|
properties: {
|
|
@@ -22,6 +24,10 @@ function getHtmlFetchToolDefinition() {
|
|
|
22
24
|
type: "string",
|
|
23
25
|
description: "URL of the website to fetch",
|
|
24
26
|
},
|
|
27
|
+
startCursor: {
|
|
28
|
+
type: "number",
|
|
29
|
+
description: "Starting cursor position in bytes. Set to 0 for initial requests, and use the value from previous responses for subsequent requests to resume content retrieval.",
|
|
30
|
+
},
|
|
25
31
|
headers: {
|
|
26
32
|
type: "object",
|
|
27
33
|
description: "Optional headers to include in the request",
|
|
@@ -56,7 +62,7 @@ function getHtmlFetchToolDefinition() {
|
|
|
56
62
|
},
|
|
57
63
|
autoDetectMode: {
|
|
58
64
|
type: "boolean",
|
|
59
|
-
description: "Optional flag to automatically switch to browser mode if standard fetch fails (default: true)",
|
|
65
|
+
description: "Optional flag to automatically switch to browser mode if standard fetch fails (default: true). Set to false to strictly use the specified mode without automatic switching.",
|
|
60
66
|
},
|
|
61
67
|
waitForSelector: {
|
|
62
68
|
type: "string",
|
|
@@ -78,8 +84,20 @@ function getHtmlFetchToolDefinition() {
|
|
|
78
84
|
type: "boolean",
|
|
79
85
|
description: "Optional flag to close the browser after fetching (default: false)",
|
|
80
86
|
},
|
|
87
|
+
contentSizeLimit: {
|
|
88
|
+
type: "number",
|
|
89
|
+
description: "Optional maximum content size in bytes before splitting into chunks (default: 50KB). Set between 20KB-50KB for optimal results. For large content, prefer smaller values (20KB-30KB) to avoid truncation.",
|
|
90
|
+
},
|
|
91
|
+
enableContentSplitting: {
|
|
92
|
+
type: "boolean",
|
|
93
|
+
description: "Optional flag to enable content splitting for large responses (default: true)",
|
|
94
|
+
},
|
|
95
|
+
chunkId: {
|
|
96
|
+
type: "string",
|
|
97
|
+
description: `Optional chunk ID for retrieving a specific chunk of content from a previous request. The system adds prompts in the format ${TemplateUtils.SYSTEM_NOTE.START} ... ${TemplateUtils.SYSTEM_NOTE.END} which AI models should ignore when processing the content.`,
|
|
98
|
+
},
|
|
81
99
|
},
|
|
82
|
-
required: ["url"],
|
|
100
|
+
required: ["url", "startCursor"],
|
|
83
101
|
},
|
|
84
102
|
};
|
|
85
103
|
}
|
|
@@ -90,7 +108,7 @@ function getHtmlFetchToolDefinition() {
|
|
|
90
108
|
function getJsonFetchToolDefinition() {
|
|
91
109
|
return {
|
|
92
110
|
name: "fetch_json",
|
|
93
|
-
description: "Fetch a JSON file from a URL",
|
|
111
|
+
description: "Fetch a JSON file from a URL. Best practices: 1) Always set startCursor=0 for initial requests, and use the fetchedBytes value from previous response for subsequent requests to ensure content continuity. 2) Set contentSizeLimit between 20000-50000 for large files. 3) When handling large content, use the chunking system by following the startCursor instructions in the system notes rather than increasing contentSizeLimit. 4) If content retrieval fails, you can retry using the same chunkId and startCursor, or adjust startCursor as needed but you must handle any resulting data duplication or gaps yourself. 5) Always explain to users when content is chunked and ask if they want to continue retrieving subsequent parts.",
|
|
94
112
|
inputSchema: {
|
|
95
113
|
type: "object",
|
|
96
114
|
properties: {
|
|
@@ -98,6 +116,10 @@ function getJsonFetchToolDefinition() {
|
|
|
98
116
|
type: "string",
|
|
99
117
|
description: "URL of the JSON to fetch",
|
|
100
118
|
},
|
|
119
|
+
startCursor: {
|
|
120
|
+
type: "number",
|
|
121
|
+
description: "Starting cursor position in bytes. Set to 0 for initial requests, and use the value from previous responses for subsequent requests to resume content retrieval.",
|
|
122
|
+
},
|
|
101
123
|
headers: {
|
|
102
124
|
type: "object",
|
|
103
125
|
description: "Optional headers to include in the request",
|
|
@@ -142,8 +164,20 @@ function getJsonFetchToolDefinition() {
|
|
|
142
164
|
type: "boolean",
|
|
143
165
|
description: "Optional flag to close the browser after fetching (default: false)",
|
|
144
166
|
},
|
|
167
|
+
contentSizeLimit: {
|
|
168
|
+
type: "number",
|
|
169
|
+
description: "Optional maximum content size in bytes before splitting into chunks (default: 50KB). Set between 20KB-50KB for optimal results. For large content, prefer smaller values (20KB-30KB) to avoid truncation.",
|
|
170
|
+
},
|
|
171
|
+
enableContentSplitting: {
|
|
172
|
+
type: "boolean",
|
|
173
|
+
description: "Optional flag to enable content splitting for large responses (default: true)",
|
|
174
|
+
},
|
|
175
|
+
chunkId: {
|
|
176
|
+
type: "string",
|
|
177
|
+
description: `Optional chunk ID for retrieving a specific chunk of content from a previous request. The system adds prompts in the format ${TemplateUtils.SYSTEM_NOTE.START} ... ${TemplateUtils.SYSTEM_NOTE.END} which AI models should ignore when processing the content.`,
|
|
178
|
+
},
|
|
145
179
|
},
|
|
146
|
-
required: ["url"],
|
|
180
|
+
required: ["url", "startCursor"],
|
|
147
181
|
},
|
|
148
182
|
};
|
|
149
183
|
}
|
|
@@ -154,7 +188,7 @@ function getJsonFetchToolDefinition() {
|
|
|
154
188
|
function getTextFetchToolDefinition() {
|
|
155
189
|
return {
|
|
156
190
|
name: "fetch_txt",
|
|
157
|
-
description: "Fetch a website, return the content as plain text (no HTML)",
|
|
191
|
+
description: "Fetch a website, return the content as plain text (no HTML). Best practices: 1) Always set startCursor=0 for initial requests, and use the fetchedBytes value from previous response for subsequent requests to ensure content continuity. 2) Set contentSizeLimit between 20000-50000 for large pages. 3) When handling large content, use the chunking system by following the startCursor instructions in the system notes rather than increasing contentSizeLimit. 4) If content retrieval fails, you can retry using the same chunkId and startCursor, or adjust startCursor as needed but you must handle any resulting data duplication or gaps yourself. 5) Always explain to users when content is chunked and ask if they want to continue retrieving subsequent parts.",
|
|
158
192
|
inputSchema: {
|
|
159
193
|
type: "object",
|
|
160
194
|
properties: {
|
|
@@ -162,6 +196,10 @@ function getTextFetchToolDefinition() {
|
|
|
162
196
|
type: "string",
|
|
163
197
|
description: "URL of the website to fetch",
|
|
164
198
|
},
|
|
199
|
+
startCursor: {
|
|
200
|
+
type: "number",
|
|
201
|
+
description: "Starting cursor position in bytes. Set to 0 for initial requests, and use the value from previous responses for subsequent requests to resume content retrieval.",
|
|
202
|
+
},
|
|
165
203
|
headers: {
|
|
166
204
|
type: "object",
|
|
167
205
|
description: "Optional headers to include in the request",
|
|
@@ -206,8 +244,20 @@ function getTextFetchToolDefinition() {
|
|
|
206
244
|
type: "boolean",
|
|
207
245
|
description: "Optional flag to scroll to bottom of page in browser mode (default: false)",
|
|
208
246
|
},
|
|
247
|
+
contentSizeLimit: {
|
|
248
|
+
type: "number",
|
|
249
|
+
description: "Optional maximum content size in bytes before splitting into chunks (default: 50KB). Set between 20KB-50KB for optimal results. For large content, prefer smaller values (20KB-30KB) to avoid truncation.",
|
|
250
|
+
},
|
|
251
|
+
enableContentSplitting: {
|
|
252
|
+
type: "boolean",
|
|
253
|
+
description: "Optional flag to enable content splitting for large responses (default: true)",
|
|
254
|
+
},
|
|
255
|
+
chunkId: {
|
|
256
|
+
type: "string",
|
|
257
|
+
description: `Optional chunk ID for retrieving a specific chunk of content from a previous request. The system adds prompts in the format ${TemplateUtils.SYSTEM_NOTE.START} ... ${TemplateUtils.SYSTEM_NOTE.END} which AI models should ignore when processing the content.`,
|
|
258
|
+
},
|
|
209
259
|
},
|
|
210
|
-
required: ["url"],
|
|
260
|
+
required: ["url", "startCursor"],
|
|
211
261
|
},
|
|
212
262
|
};
|
|
213
263
|
}
|
|
@@ -218,7 +268,7 @@ function getTextFetchToolDefinition() {
|
|
|
218
268
|
function getMarkdownFetchToolDefinition() {
|
|
219
269
|
return {
|
|
220
270
|
name: "fetch_markdown",
|
|
221
|
-
description: "Fetch a website and return the content as Markdown",
|
|
271
|
+
description: "Fetch a website and return the content as Markdown. Best practices: 1) Always set startCursor=0 for initial requests, and use the fetchedBytes value from previous response for subsequent requests to ensure content continuity. 2) Set contentSizeLimit between 20000-50000 for large pages. 3) When handling large content, use the chunking system by following the startCursor instructions in the system notes rather than increasing contentSizeLimit. 4) If content retrieval fails, you can retry using the same chunkId and startCursor, or adjust startCursor as needed but you must handle any resulting data duplication or gaps yourself. 5) Always explain to users when content is chunked and ask if they want to continue retrieving subsequent parts.",
|
|
222
272
|
inputSchema: {
|
|
223
273
|
type: "object",
|
|
224
274
|
properties: {
|
|
@@ -226,6 +276,10 @@ function getMarkdownFetchToolDefinition() {
|
|
|
226
276
|
type: "string",
|
|
227
277
|
description: "URL of the website to fetch",
|
|
228
278
|
},
|
|
279
|
+
startCursor: {
|
|
280
|
+
type: "number",
|
|
281
|
+
description: "Starting cursor position in bytes. Set to 0 for initial requests, and use the value from previous responses for subsequent requests to resume content retrieval.",
|
|
282
|
+
},
|
|
229
283
|
headers: {
|
|
230
284
|
type: "object",
|
|
231
285
|
description: "Optional headers to include in the request",
|
|
@@ -270,8 +324,112 @@ function getMarkdownFetchToolDefinition() {
|
|
|
270
324
|
type: "boolean",
|
|
271
325
|
description: "Optional flag to scroll to bottom of page in browser mode (default: false)",
|
|
272
326
|
},
|
|
327
|
+
contentSizeLimit: {
|
|
328
|
+
type: "number",
|
|
329
|
+
description: "Optional maximum content size in bytes before splitting into chunks (default: 50KB). Set between 20KB-50KB for optimal results. For large content, prefer smaller values (20KB-30KB) to avoid truncation.",
|
|
330
|
+
},
|
|
331
|
+
enableContentSplitting: {
|
|
332
|
+
type: "boolean",
|
|
333
|
+
description: "Optional flag to enable content splitting for large responses (default: true)",
|
|
334
|
+
},
|
|
335
|
+
chunkId: {
|
|
336
|
+
type: "string",
|
|
337
|
+
description: `Optional chunk ID for retrieving a specific chunk of content from a previous request. The system adds prompts in the format ${TemplateUtils.SYSTEM_NOTE.START} ... ${TemplateUtils.SYSTEM_NOTE.END} which AI models should ignore when processing the content.`,
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
required: ["url", "startCursor"],
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* 获取纯文本获取工具定义 (Get plain text fetch tool definition)
|
|
346
|
+
* @returns 纯文本获取工具定义 (Plain text fetch tool definition)
|
|
347
|
+
*/
|
|
348
|
+
function getPlainTextFetchToolDefinition() {
|
|
349
|
+
return {
|
|
350
|
+
name: "fetch_plaintext",
|
|
351
|
+
description: "Fetch a website and return the content as plain text with HTML tags removed. Best practices: 1) Always set startCursor=0 for initial requests, and use the fetchedBytes value from previous response for subsequent requests to ensure content continuity. 2) Set contentSizeLimit between 20000-50000 for large pages. 3) When handling large content, use the chunking system by following the startCursor instructions in the system notes rather than increasing contentSizeLimit. 4) If content retrieval fails, you can retry using the same chunkId and startCursor, or adjust startCursor as needed but you must handle any resulting data duplication or gaps yourself. 5) Always explain to users when content is chunked and ask if they want to continue retrieving subsequent parts.",
|
|
352
|
+
inputSchema: {
|
|
353
|
+
type: "object",
|
|
354
|
+
properties: {
|
|
355
|
+
url: {
|
|
356
|
+
type: "string",
|
|
357
|
+
description: "URL of the website to fetch",
|
|
358
|
+
},
|
|
359
|
+
startCursor: {
|
|
360
|
+
type: "number",
|
|
361
|
+
description: "Starting cursor position in bytes. Set to 0 for initial requests, and use the value from previous responses for subsequent requests to resume content retrieval.",
|
|
362
|
+
},
|
|
363
|
+
headers: {
|
|
364
|
+
type: "object",
|
|
365
|
+
description: "Optional headers to include in the request",
|
|
366
|
+
},
|
|
367
|
+
proxy: {
|
|
368
|
+
type: "string",
|
|
369
|
+
description: "Optional proxy server to use (format: http://host:port or https://host:port)",
|
|
370
|
+
},
|
|
371
|
+
noDelay: {
|
|
372
|
+
type: "boolean",
|
|
373
|
+
description: "Optional flag to disable random delay between requests (default: false)",
|
|
374
|
+
},
|
|
375
|
+
timeout: {
|
|
376
|
+
type: "number",
|
|
377
|
+
description: "Optional timeout in milliseconds (default: 30000)",
|
|
378
|
+
},
|
|
379
|
+
maxRedirects: {
|
|
380
|
+
type: "number",
|
|
381
|
+
description: "Optional maximum number of redirects to follow (default: 10)",
|
|
382
|
+
},
|
|
383
|
+
useSystemProxy: {
|
|
384
|
+
type: "boolean",
|
|
385
|
+
description: "Optional flag to use system proxy environment variables (default: true)",
|
|
386
|
+
},
|
|
387
|
+
debug: {
|
|
388
|
+
type: "boolean",
|
|
389
|
+
description: "Optional flag to enable detailed debug logging (default: false)",
|
|
390
|
+
},
|
|
391
|
+
useBrowser: {
|
|
392
|
+
type: "boolean",
|
|
393
|
+
description: "Optional flag to use headless browser for fetching (default: false)",
|
|
394
|
+
},
|
|
395
|
+
autoDetectMode: {
|
|
396
|
+
type: "boolean",
|
|
397
|
+
description: "Optional flag to automatically switch to browser mode if standard fetch fails (default: true). Set to false to strictly use the specified mode without automatic switching.",
|
|
398
|
+
},
|
|
399
|
+
waitForSelector: {
|
|
400
|
+
type: "string",
|
|
401
|
+
description: "Optional CSS selector to wait for when using browser mode",
|
|
402
|
+
},
|
|
403
|
+
waitForTimeout: {
|
|
404
|
+
type: "number",
|
|
405
|
+
description: "Optional timeout to wait after page load in browser mode (default: 5000)",
|
|
406
|
+
},
|
|
407
|
+
scrollToBottom: {
|
|
408
|
+
type: "boolean",
|
|
409
|
+
description: "Optional flag to scroll to bottom of page in browser mode (default: false)",
|
|
410
|
+
},
|
|
411
|
+
saveCookies: {
|
|
412
|
+
type: "boolean",
|
|
413
|
+
description: "Optional flag to save cookies for future requests to the same domain (default: true)",
|
|
414
|
+
},
|
|
415
|
+
closeBrowser: {
|
|
416
|
+
type: "boolean",
|
|
417
|
+
description: "Optional flag to close the browser after fetching (default: false)",
|
|
418
|
+
},
|
|
419
|
+
contentSizeLimit: {
|
|
420
|
+
type: "number",
|
|
421
|
+
description: "Optional maximum content size in bytes before splitting into chunks (default: 50KB). Set between 20KB-50KB for optimal results. For large content, prefer smaller values (20KB-30KB) to avoid truncation.",
|
|
422
|
+
},
|
|
423
|
+
enableContentSplitting: {
|
|
424
|
+
type: "boolean",
|
|
425
|
+
description: "Optional flag to enable content splitting for large responses (default: true)",
|
|
426
|
+
},
|
|
427
|
+
chunkId: {
|
|
428
|
+
type: "string",
|
|
429
|
+
description: `Optional chunk ID for retrieving a specific chunk of content from a previous request. The system adds prompts in the format ${TemplateUtils.SYSTEM_NOTE.START} ... ${TemplateUtils.SYSTEM_NOTE.END} which AI models should ignore when processing the content.`,
|
|
430
|
+
},
|
|
273
431
|
},
|
|
274
|
-
required: ["url"],
|
|
432
|
+
required: ["url", "startCursor"],
|
|
275
433
|
},
|
|
276
434
|
};
|
|
277
435
|
}
|
|
@@ -284,7 +442,8 @@ function getAllToolDefinitions() {
|
|
|
284
442
|
getHtmlFetchToolDefinition(),
|
|
285
443
|
getJsonFetchToolDefinition(),
|
|
286
444
|
getTextFetchToolDefinition(),
|
|
287
|
-
getMarkdownFetchToolDefinition()
|
|
445
|
+
getMarkdownFetchToolDefinition(),
|
|
446
|
+
getPlainTextFetchToolDefinition()
|
|
288
447
|
];
|
|
289
448
|
}
|
|
290
449
|
/**
|
|
@@ -310,7 +469,7 @@ function registerToolCallHandler(server) {
|
|
|
310
469
|
log('tools.callReceived', debug, { name, args: JSON.stringify(args) }, COMPONENTS.SERVER);
|
|
311
470
|
try {
|
|
312
471
|
// 处理不同类型的获取请求 (Handle different types of fetch requests)
|
|
313
|
-
if (name === 'fetch_html' || name === 'fetch_json' || name === 'fetch_txt' || name === 'fetch_markdown') {
|
|
472
|
+
if (name === 'fetch_html' || name === 'fetch_json' || name === 'fetch_txt' || name === 'fetch_markdown' || name === 'fetch_plaintext') {
|
|
314
473
|
const result = await handleFetchRequest(name, args, debug);
|
|
315
474
|
// 直接返回符合 MCP SDK 要求的格式
|
|
316
475
|
return result;
|
|
@@ -318,29 +477,13 @@ function registerToolCallHandler(server) {
|
|
|
318
477
|
else {
|
|
319
478
|
// 未知工具 (Unknown tool)
|
|
320
479
|
log('tools.unknownTool', debug, { name }, COMPONENTS.SERVER);
|
|
321
|
-
return {
|
|
322
|
-
content: [
|
|
323
|
-
{
|
|
324
|
-
type: "text",
|
|
325
|
-
text: `Unknown tool: ${name}`
|
|
326
|
-
}
|
|
327
|
-
],
|
|
328
|
-
isError: true
|
|
329
|
-
};
|
|
480
|
+
return BaseFetcher.createErrorResponse(`Unknown tool: ${name}`);
|
|
330
481
|
}
|
|
331
482
|
}
|
|
332
483
|
catch (error) {
|
|
333
484
|
// 处理错误 (Handle error)
|
|
334
485
|
log('tools.callError', debug, { name, error: error.message }, COMPONENTS.SERVER);
|
|
335
|
-
return {
|
|
336
|
-
content: [
|
|
337
|
-
{
|
|
338
|
-
type: "text",
|
|
339
|
-
text: error.message
|
|
340
|
-
}
|
|
341
|
-
],
|
|
342
|
-
isError: true
|
|
343
|
-
};
|
|
486
|
+
return BaseFetcher.createErrorResponse(`Error fetching ${args.url || `chunk ${args.chunkId}`}: ${error.message}`);
|
|
344
487
|
}
|
|
345
488
|
finally {
|
|
346
489
|
// 如果请求参数中指定了关闭浏览器,则关闭浏览器 (If request parameters specify to close browser, close browser)
|
|
@@ -359,20 +502,22 @@ function registerToolCallHandler(server) {
|
|
|
359
502
|
*/
|
|
360
503
|
async function handleFetchRequest(name, args, debug) {
|
|
361
504
|
// 验证URL参数 (Validate URL parameter)
|
|
362
|
-
if (!args.url) {
|
|
363
|
-
log('tools.
|
|
364
|
-
return
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
],
|
|
371
|
-
isError: true
|
|
372
|
-
};
|
|
505
|
+
if (!args.url && !args.chunkId) {
|
|
506
|
+
log('tools.missingUrlOrChunkId', debug, {}, COMPONENTS.SERVER);
|
|
507
|
+
return BaseFetcher.createErrorResponse("Either URL or chunkId parameter is required");
|
|
508
|
+
}
|
|
509
|
+
// 验证startCursor参数 (Validate startCursor parameter)
|
|
510
|
+
if (args.startCursor === undefined) {
|
|
511
|
+
log('tools.missingStartCursor', debug, {}, COMPONENTS.SERVER);
|
|
512
|
+
return BaseFetcher.createErrorResponse("startCursor parameter is required. Use 0 for initial requests.");
|
|
373
513
|
}
|
|
374
514
|
// 记录请求 (Log request)
|
|
375
|
-
|
|
515
|
+
if (args.chunkId) {
|
|
516
|
+
log('tools.fetchChunkRequest', debug, { chunkId: args.chunkId, startCursor: args.startCursor, type: name }, COMPONENTS.SERVER);
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
log('tools.fetchRequest', debug, { url: args.url, startCursor: args.startCursor, type: name }, COMPONENTS.SERVER);
|
|
520
|
+
}
|
|
376
521
|
// 执行获取请求 (Execute fetch request)
|
|
377
522
|
try {
|
|
378
523
|
const params = {
|
|
@@ -381,7 +526,7 @@ async function handleFetchRequest(name, args, debug) {
|
|
|
381
526
|
};
|
|
382
527
|
// 根据工具名称确定内容类型 (Determine content type based on tool name)
|
|
383
528
|
const type = name.replace('fetch_', '');
|
|
384
|
-
|
|
529
|
+
let result = await fetchWithAutoDetect(params, type);
|
|
385
530
|
// 确保返回标准结构体 (Ensure returning standard structure)
|
|
386
531
|
if (result.isError) {
|
|
387
532
|
// 确保错误内容有正确的类型 (Ensure error content has correct type)
|
|
@@ -407,6 +552,19 @@ async function handleFetchRequest(name, args, debug) {
|
|
|
407
552
|
if (!result.content[0].type) {
|
|
408
553
|
result.content[0].type = "text";
|
|
409
554
|
}
|
|
555
|
+
// 如果内容是分段的,添加提示词 (If content is chunked, add prompt)
|
|
556
|
+
if (result.isChunked && result.hasMoreChunks) {
|
|
557
|
+
// 检查内容中是否已经包含系统提示,避免重复添加
|
|
558
|
+
// Check if content already contains system note to avoid duplicate
|
|
559
|
+
if (!TemplateUtils.hasSystemPrompt(result.content[0].text || '')) {
|
|
560
|
+
// 使用BaseFetcher的addChunkPrompt方法添加提示词 (Use BaseFetcher's addChunkPrompt method to add prompt)
|
|
561
|
+
result = BaseFetcher.addChunkPrompt(result);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
// 如果没有匹配的类型,返回错误
|
|
566
|
+
if (!result.content[0].type) {
|
|
567
|
+
return BaseFetcher.createErrorResponse(`Unsupported content type: ${type}`);
|
|
410
568
|
}
|
|
411
569
|
// 将FetchResponse转换为符合MCP SDK要求的格式
|
|
412
570
|
return {
|
|
@@ -417,16 +575,8 @@ async function handleFetchRequest(name, args, debug) {
|
|
|
417
575
|
catch (error) {
|
|
418
576
|
// 处理错误 (Handle error)
|
|
419
577
|
log('tools.fetchError', debug, { url: args.url, error: error.message }, COMPONENTS.SERVER);
|
|
420
|
-
//
|
|
421
|
-
return {
|
|
422
|
-
content: [
|
|
423
|
-
{
|
|
424
|
-
type: "text",
|
|
425
|
-
text: `Error fetching ${args.url}: ${error.message}`
|
|
426
|
-
}
|
|
427
|
-
],
|
|
428
|
-
isError: true
|
|
429
|
-
};
|
|
578
|
+
// 返回错误信息 (Return error message)
|
|
579
|
+
return BaseFetcher.createErrorResponse(`Error fetching ${args.url || `chunk ${args.chunkId}`}: ${error.message}`);
|
|
430
580
|
}
|
|
431
581
|
}
|
|
432
582
|
/**
|
package/dist/src/lib/types.js
CHANGED
|
@@ -32,4 +32,9 @@ export const RequestPayloadSchema = z.object({
|
|
|
32
32
|
waitForTimeout: z.number().optional(),
|
|
33
33
|
scrollToBottom: z.boolean().optional(),
|
|
34
34
|
closeBrowser: z.boolean().optional(),
|
|
35
|
+
chunkId: z.string().optional(),
|
|
36
|
+
chunkIndex: z.number().optional(),
|
|
37
|
+
contentSizeLimit: z.number().optional(),
|
|
38
|
+
startCursor: z.number().optional(),
|
|
39
|
+
enableContentSplitting: z.boolean().optional(),
|
|
35
40
|
}).merge(BrowserParamsSchema);
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Author: Martin <lmccc.dev@gmail.com>
|
|
3
|
+
* Co-Author: AI Assistant (Claude)
|
|
4
|
+
* Description: This code was collaboratively developed by Martin and AI Assistant.
|
|
5
|
+
*/
|
|
6
|
+
import { log, COMPONENTS } from '../logger.js';
|
|
7
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
8
|
+
/**
|
|
9
|
+
* 分段内容管理器 (Chunk content manager)
|
|
10
|
+
* 用于存储和管理分段内容 (Used to store and manage chunked content)
|
|
11
|
+
*/
|
|
12
|
+
export class ChunkManager {
|
|
13
|
+
/**
|
|
14
|
+
* 存储分段内容的Map (Map to store chunked content)
|
|
15
|
+
* 键为分段ID,值为分段内容数组 (Key is chunk ID, value is array of chunk content)
|
|
16
|
+
*/
|
|
17
|
+
static chunks = new Map();
|
|
18
|
+
/**
|
|
19
|
+
* 存储分段内容大小信息的Map (Map to store chunk size information)
|
|
20
|
+
* 键为分段ID,值为{totalBytes, fetchedBytes}对象 (Key is chunk ID, value is {totalBytes, fetchedBytes} object)
|
|
21
|
+
*/
|
|
22
|
+
static sizeInfo = new Map();
|
|
23
|
+
/**
|
|
24
|
+
* 分段内容的过期时间(毫秒) (Expiration time for chunked content in milliseconds)
|
|
25
|
+
* 默认为10分钟 (Default is 10 minutes)
|
|
26
|
+
*/
|
|
27
|
+
static EXPIRATION_TIME = 10 * 60 * 1000; // 10分钟
|
|
28
|
+
/**
|
|
29
|
+
* 分段内容的过期时间Map (Map to store expiration time for chunked content)
|
|
30
|
+
* 键为分段ID,值为过期时间戳 (Key is chunk ID, value is expiration timestamp)
|
|
31
|
+
*/
|
|
32
|
+
static expirations = new Map();
|
|
33
|
+
/**
|
|
34
|
+
* 存储分段内容 (Store chunked content)
|
|
35
|
+
* @param chunks 分段内容数组 (Array of chunk content)
|
|
36
|
+
* @param totalBytes 原始内容总字节数 (Total bytes of original content)
|
|
37
|
+
* @param debug 是否启用调试模式 (Whether debug mode is enabled)
|
|
38
|
+
* @returns 分段ID (Chunk ID)
|
|
39
|
+
*/
|
|
40
|
+
static storeChunks(chunks, totalBytes, debug = false) {
|
|
41
|
+
// 清理过期的分段内容 (Clean up expired chunks)
|
|
42
|
+
this.cleanupExpiredChunks(debug);
|
|
43
|
+
// 生成唯一的分段ID (Generate unique chunk ID)
|
|
44
|
+
const chunkId = uuidv4();
|
|
45
|
+
// 计算每个分块的字节大小 (Calculate byte size of each chunk)
|
|
46
|
+
const chunkSizes = chunks.map(chunk => Buffer.byteLength(chunk, 'utf8'));
|
|
47
|
+
// 存储分段内容 (Store chunked content)
|
|
48
|
+
this.chunks.set(chunkId, chunks);
|
|
49
|
+
// 存储分段大小信息 (Store chunk size information)
|
|
50
|
+
this.sizeInfo.set(chunkId, {
|
|
51
|
+
totalBytes,
|
|
52
|
+
fetchedBytes: chunkSizes
|
|
53
|
+
});
|
|
54
|
+
// 设置过期时间 (Set expiration time)
|
|
55
|
+
const expirationTime = Date.now() + this.EXPIRATION_TIME;
|
|
56
|
+
this.expirations.set(chunkId, expirationTime);
|
|
57
|
+
log('chunkManager.storedChunks', debug, {
|
|
58
|
+
chunkId,
|
|
59
|
+
count: chunks.length,
|
|
60
|
+
totalChunks: chunks.length,
|
|
61
|
+
totalBytes,
|
|
62
|
+
expiresAt: new Date(expirationTime).toISOString()
|
|
63
|
+
}, COMPONENTS.CHUNK_MANAGER);
|
|
64
|
+
return chunkId;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 获取分段内容 (Get chunked content)
|
|
68
|
+
* @param chunkId 分段ID (Chunk ID)
|
|
69
|
+
* @param startCursor 开始游标位置,指示从哪个字节开始获取 (Start cursor position, indicating from which byte to start fetching)
|
|
70
|
+
* @param sizeLimit 本次获取的最大字节数 (Maximum bytes to fetch in this request)
|
|
71
|
+
* @param debug 是否启用调试模式 (Whether debug mode is enabled)
|
|
72
|
+
* @returns 分段内容,包含获取信息 (Chunk content with fetch information)
|
|
73
|
+
*/
|
|
74
|
+
static getChunkBySize(chunkId, startCursor = 0, sizeLimit = 50 * 1024, debug = false) {
|
|
75
|
+
// 检查分段ID是否存在 (Check if chunk ID exists)
|
|
76
|
+
if (!this.chunks.has(chunkId) || !this.sizeInfo.has(chunkId)) {
|
|
77
|
+
log('chunkManager.chunkIdNotFound', debug, { chunkId }, COMPONENTS.CHUNK_MANAGER);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
// 获取分段内容数组和大小信息 (Get chunked content array and size information)
|
|
81
|
+
const chunks = this.chunks.get(chunkId);
|
|
82
|
+
const { totalBytes, fetchedBytes } = this.sizeInfo.get(chunkId);
|
|
83
|
+
// 验证startCursor是否有效 (Validate if startCursor is valid)
|
|
84
|
+
if (startCursor < 0 || startCursor >= totalBytes) {
|
|
85
|
+
log('chunkManager.invalidStartCursor', debug, {
|
|
86
|
+
chunkId,
|
|
87
|
+
startCursor,
|
|
88
|
+
totalBytes
|
|
89
|
+
}, COMPONENTS.CHUNK_MANAGER);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
// 计算当前位置和每个分块的起始位置 (Calculate current position and start position of each chunk)
|
|
93
|
+
let currentPosition = 0;
|
|
94
|
+
let chunkIndex = 0;
|
|
95
|
+
let fetchedSoFar = 0;
|
|
96
|
+
let resultContent = '';
|
|
97
|
+
// 找到开始位置对应的分块 (Find the chunk corresponding to the start position)
|
|
98
|
+
for (let i = 0; i < fetchedBytes.length; i++) {
|
|
99
|
+
if (currentPosition + fetchedBytes[i] > startCursor) {
|
|
100
|
+
chunkIndex = i;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
fetchedSoFar += fetchedBytes[i];
|
|
104
|
+
currentPosition += fetchedBytes[i];
|
|
105
|
+
}
|
|
106
|
+
// 从找到的分块开始,读取指定大小的内容 (Start reading from the found chunk, up to the specified size)
|
|
107
|
+
let bytesToFetch = sizeLimit;
|
|
108
|
+
let bytesActuallyFetched = 0;
|
|
109
|
+
while (chunkIndex < chunks.length && bytesToFetch > 0) {
|
|
110
|
+
resultContent += chunks[chunkIndex];
|
|
111
|
+
bytesActuallyFetched += fetchedBytes[chunkIndex];
|
|
112
|
+
bytesToFetch -= fetchedBytes[chunkIndex];
|
|
113
|
+
chunkIndex++;
|
|
114
|
+
}
|
|
115
|
+
// 计算总获取字节数和剩余字节数 (Calculate total fetched bytes and remaining bytes)
|
|
116
|
+
const totalFetchedBytes = startCursor + bytesActuallyFetched;
|
|
117
|
+
const remainingBytes = totalBytes - totalFetchedBytes;
|
|
118
|
+
const isLastChunk = (remainingBytes <= 0);
|
|
119
|
+
log('chunkManager.retrievedChunkBySize', debug, {
|
|
120
|
+
chunkId,
|
|
121
|
+
startCursor,
|
|
122
|
+
size: sizeLimit,
|
|
123
|
+
bytesRequested: sizeLimit,
|
|
124
|
+
bytesRetrieved: bytesActuallyFetched,
|
|
125
|
+
totalFetchedBytes,
|
|
126
|
+
remainingBytes,
|
|
127
|
+
isLastChunk
|
|
128
|
+
}, COMPONENTS.CHUNK_MANAGER);
|
|
129
|
+
// 返回内容和获取信息 (Return content and fetch information)
|
|
130
|
+
return {
|
|
131
|
+
content: resultContent,
|
|
132
|
+
fetchedBytes: totalFetchedBytes,
|
|
133
|
+
remainingBytes,
|
|
134
|
+
isLastChunk,
|
|
135
|
+
totalBytes
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* 获取分段大小信息 (Get chunk size information)
|
|
140
|
+
* @param chunkId 分段ID (Chunk ID)
|
|
141
|
+
* @param debug 是否启用调试模式 (Whether debug mode is enabled)
|
|
142
|
+
* @returns 分段大小信息 (Chunk size information)
|
|
143
|
+
*/
|
|
144
|
+
static getSizeInfo(chunkId, debug = false) {
|
|
145
|
+
// 检查分段ID是否存在 (Check if chunk ID exists)
|
|
146
|
+
if (!this.sizeInfo.has(chunkId)) {
|
|
147
|
+
log('chunkManager.sizeInfoNotFound', debug, { chunkId }, COMPONENTS.CHUNK_MANAGER);
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
// 返回分段大小信息 (Return chunk size information)
|
|
151
|
+
return this.sizeInfo.get(chunkId);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* 清理过期的分段内容 (Clean up expired chunks)
|
|
155
|
+
* @param debug 是否启用调试模式 (Whether debug mode is enabled)
|
|
156
|
+
*/
|
|
157
|
+
static cleanupExpiredChunks(debug = false) {
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
let expiredCount = 0;
|
|
160
|
+
// 遍历所有分段ID (Iterate through all chunk IDs)
|
|
161
|
+
for (const [chunkId, expirationTime] of this.expirations.entries()) {
|
|
162
|
+
// 如果分段内容已过期,则删除 (If chunk content has expired, delete it)
|
|
163
|
+
if (expirationTime < now) {
|
|
164
|
+
this.chunks.delete(chunkId);
|
|
165
|
+
this.sizeInfo.delete(chunkId);
|
|
166
|
+
this.expirations.delete(chunkId);
|
|
167
|
+
expiredCount++;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (expiredCount > 0) {
|
|
171
|
+
log('chunkManager.cleanedUpExpiredChunks', debug, {
|
|
172
|
+
expiredCount,
|
|
173
|
+
remainingChunks: this.chunks.size
|
|
174
|
+
}, COMPONENTS.CHUNK_MANAGER);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|