@lmcc-dev/mult-fetch-mcp-server 1.0.3 → 1.1.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 +3 -1
- package/README.zh.md +3 -2
- package/dist/src/client.d.ts +6 -0
- package/dist/src/client.js +26 -28
- package/dist/src/client.js.map +1 -0
- package/dist/src/index.d.ts +15 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lib/BrowserFetcher.d.ts +175 -0
- package/dist/src/lib/BrowserFetcher.js.map +1 -0
- package/dist/src/lib/NodeFetcher.d.ts +92 -0
- package/dist/src/lib/NodeFetcher.js +143 -84
- package/dist/src/lib/NodeFetcher.js.map +1 -0
- package/dist/src/lib/i18n/index.js.map +1 -0
- package/dist/src/lib/i18n/keys.js +135 -0
- package/dist/src/lib/i18n/locales/en/browser.d.ts +61 -0
- package/dist/src/lib/i18n/locales/en/browser.js +68 -54
- package/dist/src/lib/i18n/locales/en/browser.js.map +1 -0
- package/dist/src/lib/i18n/locales/en/client.d.ts +39 -0
- package/dist/src/lib/i18n/locales/en/client.js.map +1 -0
- package/dist/src/lib/i18n/locales/en/errors.d.ts +17 -0
- package/dist/src/lib/i18n/locales/en/errors.js.map +1 -0
- package/dist/src/lib/i18n/locales/en/fetcher.d.ts +66 -0
- package/dist/src/lib/i18n/locales/en/fetcher.js +60 -59
- package/dist/src/lib/i18n/locales/en/fetcher.js.map +1 -0
- package/dist/src/lib/i18n/locales/en/index.d.ts +344 -0
- package/dist/src/lib/i18n/locales/en/index.js +1 -1
- package/dist/src/lib/i18n/locales/en/index.js.map +1 -0
- package/dist/src/lib/i18n/locales/en/node.d.ts +41 -0
- package/dist/src/lib/i18n/locales/en/node.js.map +1 -0
- package/dist/src/lib/i18n/locales/en/prompts.d.ts +65 -0
- package/dist/src/lib/i18n/locales/en/prompts.js.map +1 -0
- package/dist/src/lib/i18n/locales/en/resources.d.ts +41 -0
- package/dist/src/lib/i18n/locales/en/resources.js +30 -15
- package/dist/src/lib/i18n/locales/en/resources.js.map +1 -0
- package/dist/src/lib/i18n/locales/en/server.d.ts +30 -0
- package/dist/src/lib/i18n/locales/en/server.js.map +1 -0
- package/dist/src/lib/i18n/locales/en/tools.d.ts +24 -0
- package/dist/src/lib/i18n/locales/en/tools.js.map +1 -0
- package/dist/src/lib/i18n/locales/zh/browser.d.ts +61 -0
- package/dist/src/lib/i18n/locales/zh/browser.js +68 -54
- package/dist/src/lib/i18n/locales/zh/browser.js.map +1 -0
- package/dist/src/lib/i18n/locales/zh/client.d.ts +38 -0
- package/dist/src/lib/i18n/locales/zh/client.js.map +1 -0
- package/dist/src/lib/i18n/locales/zh/errors.d.ts +17 -0
- package/dist/src/lib/i18n/locales/zh/errors.js.map +1 -0
- package/dist/src/lib/i18n/locales/zh/fetcher.d.ts +66 -0
- package/dist/src/lib/i18n/locales/zh/fetcher.js +60 -59
- package/dist/src/lib/i18n/locales/zh/fetcher.js.map +1 -0
- package/dist/src/lib/i18n/locales/zh/index.d.ts +343 -0
- package/dist/src/lib/i18n/locales/zh/index.js +1 -1
- package/dist/src/lib/i18n/locales/zh/index.js.map +1 -0
- package/dist/src/lib/i18n/locales/zh/node.d.ts +41 -0
- package/dist/src/lib/i18n/locales/zh/node.js.map +1 -0
- package/dist/src/lib/i18n/locales/zh/prompts.d.ts +65 -0
- package/dist/src/lib/i18n/locales/zh/prompts.js.map +1 -0
- package/dist/src/lib/i18n/locales/zh/resources.d.ts +41 -0
- package/dist/src/lib/i18n/locales/zh/resources.js +28 -13
- package/dist/src/lib/i18n/locales/zh/resources.js.map +1 -0
- package/dist/src/lib/i18n/locales/zh/server.d.ts +30 -0
- package/dist/src/lib/i18n/locales/zh/server.js.map +1 -0
- package/dist/src/lib/i18n/locales/zh/tools.d.ts +24 -0
- package/dist/src/lib/i18n/locales/zh/tools.js.map +1 -0
- package/dist/src/lib/i18n/logger.d.ts +38 -0
- package/dist/src/lib/i18n/logger.js.map +1 -0
- package/dist/src/lib/logger.d.ts +32 -0
- package/dist/src/lib/logger.js.map +1 -0
- package/dist/src/lib/server/browser.d.ts +34 -0
- package/dist/src/lib/server/browser.js +10 -16
- package/dist/src/lib/server/browser.js.map +1 -0
- package/dist/src/lib/server/fetcher.d.ts +20 -0
- package/dist/src/lib/server/fetcher.js.map +1 -0
- package/dist/src/lib/server/index.d.ts +10 -0
- package/dist/src/lib/server/index.js.map +1 -0
- package/dist/src/lib/server/prompts.d.ts +11 -0
- package/dist/src/lib/server/prompts.js.map +1 -0
- package/dist/src/lib/server/resources.d.ts +11 -0
- package/dist/src/lib/server/resources.js.map +1 -0
- package/dist/src/lib/server/tools.d.ts +12 -0
- package/dist/src/lib/server/tools.js +369 -309
- package/dist/src/lib/server/tools.js.map +1 -0
- package/dist/src/lib/server/types.d.ts +71 -0
- package/dist/src/lib/server/types.js.map +1 -0
- package/dist/src/lib/types.d.ts +119 -0
- package/dist/src/lib/types.js.map +1 -0
- package/dist/src/lib/utils/errorDetection.js +41 -0
- package/dist/src/lib/utils/errors.js +279 -0
- package/dist/src/mcp-server.d.ts +6 -0
- package/dist/src/mcp-server.js.map +1 -0
- package/dist/tests/BrowserFetcher.test.js +951 -0
- package/dist/tests/NodeFetcher.test.js +263 -0
- package/dist/tests/client.test.js +250 -0
- package/dist/tests/i18n.test.js +179 -0
- package/dist/tests/jest.setup.d.ts +6 -0
- package/dist/tests/jest.setup.js +30 -0
- package/dist/tests/jest.setup.js.map +1 -0
- package/dist/tests/logger.test.js +99 -0
- package/dist/tests/mcp-server.test.js +113 -0
- package/dist/tests/server/browser.test.js +197 -0
- package/dist/tests/server/fetcher.test.js +216 -0
- package/dist/tests/server/index.test.js +122 -0
- package/dist/tests/server/prompts.test.js +243 -0
- package/dist/tests/server/resources.test.js +359 -0
- package/dist/tests/server/tools.test.js +222 -0
- package/dist/tests/test-direct-client.d.ts +6 -0
- package/dist/tests/test-direct-client.js.map +1 -0
- package/dist/tests/test-mcp-methods.d.ts +6 -0
- package/dist/tests/test-mcp-methods.js.map +1 -0
- package/dist/tests/test-mcp.d.ts +6 -0
- package/dist/tests/test-mcp.js.map +1 -0
- package/dist/tests/test-mini4k.d.ts +6 -0
- package/dist/tests/test-mini4k.js.map +1 -0
- package/dist/tests/types.test.js +57 -0
- package/package.json +11 -2
|
@@ -0,0 +1,263 @@
|
|
|
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 { NodeFetcher } from '../src/lib/NodeFetcher.js';
|
|
7
|
+
// 模拟node-fetch
|
|
8
|
+
jest.mock('node-fetch', () => {
|
|
9
|
+
const mockFetch = jest.fn();
|
|
10
|
+
// 创建模拟Response
|
|
11
|
+
const createMockResponse = (url) => {
|
|
12
|
+
// 根据URL返回不同的内容
|
|
13
|
+
let textContent = '<html><body><h1>测试页面</h1></body></html>';
|
|
14
|
+
let jsonContent = { test: 'data' };
|
|
15
|
+
let contentType = 'text/html; charset=utf-8';
|
|
16
|
+
if (url.includes('/api')) {
|
|
17
|
+
textContent = JSON.stringify(jsonContent);
|
|
18
|
+
contentType = 'application/json; charset=utf-8';
|
|
19
|
+
}
|
|
20
|
+
else if (url.includes('/text')) {
|
|
21
|
+
textContent = '这是纯文本内容';
|
|
22
|
+
contentType = 'text/plain; charset=utf-8';
|
|
23
|
+
}
|
|
24
|
+
else if (url.includes('/md')) {
|
|
25
|
+
textContent = '# 标题\n\n这是Markdown内容';
|
|
26
|
+
contentType = 'text/markdown; charset=utf-8';
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
ok: true,
|
|
30
|
+
status: 200,
|
|
31
|
+
statusText: 'OK',
|
|
32
|
+
url: url,
|
|
33
|
+
headers: {
|
|
34
|
+
get: jest.fn((name) => {
|
|
35
|
+
if (name.toLowerCase() === 'content-type') {
|
|
36
|
+
return contentType;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}),
|
|
40
|
+
raw: () => ({}),
|
|
41
|
+
entries: () => [['content-type', contentType]]
|
|
42
|
+
},
|
|
43
|
+
text: jest.fn().mockResolvedValue(textContent),
|
|
44
|
+
json: jest.fn().mockResolvedValue(jsonContent),
|
|
45
|
+
buffer: jest.fn().mockResolvedValue(Buffer.from(textContent)),
|
|
46
|
+
clone: jest.fn(function () { return this; })
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
// 设置默认返回值
|
|
50
|
+
mockFetch.mockImplementation((url) => {
|
|
51
|
+
return Promise.resolve(createMockResponse(url));
|
|
52
|
+
});
|
|
53
|
+
return mockFetch;
|
|
54
|
+
});
|
|
55
|
+
// 模拟http-proxy-agent和https-proxy-agent
|
|
56
|
+
jest.mock('http-proxy-agent', () => ({
|
|
57
|
+
HttpProxyAgent: jest.fn().mockImplementation(() => ({}))
|
|
58
|
+
}));
|
|
59
|
+
jest.mock('https-proxy-agent', () => ({
|
|
60
|
+
HttpsProxyAgent: jest.fn().mockImplementation(() => ({}))
|
|
61
|
+
}));
|
|
62
|
+
// 模拟child_process
|
|
63
|
+
jest.mock('child_process', () => ({
|
|
64
|
+
execSync: jest.fn().mockReturnValue(Buffer.from(''))
|
|
65
|
+
}));
|
|
66
|
+
// 模拟i18n
|
|
67
|
+
jest.mock('../src/lib/i18n/index.js', () => ({
|
|
68
|
+
t: (key, options) => {
|
|
69
|
+
return options ? `${key} ${JSON.stringify(options)}` : key;
|
|
70
|
+
}
|
|
71
|
+
}));
|
|
72
|
+
// 模拟logger
|
|
73
|
+
jest.mock('../src/lib/logger.js', () => ({
|
|
74
|
+
log: jest.fn(),
|
|
75
|
+
COMPONENTS: {
|
|
76
|
+
NODE_FETCH: 'NODE-FETCH'
|
|
77
|
+
}
|
|
78
|
+
}));
|
|
79
|
+
describe('NodeFetcher类测试 (NodeFetcher Class Tests)', () => {
|
|
80
|
+
let fetch;
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
// 重置所有模拟
|
|
83
|
+
jest.clearAllMocks();
|
|
84
|
+
// 获取模拟的fetch函数
|
|
85
|
+
fetch = require('node-fetch');
|
|
86
|
+
});
|
|
87
|
+
describe('基本功能测试 (Basic Functionality Tests)', () => {
|
|
88
|
+
test('应该有html方法 (Should have html method)', () => {
|
|
89
|
+
expect(typeof NodeFetcher.html).toBe('function');
|
|
90
|
+
});
|
|
91
|
+
test('应该有json方法 (Should have json method)', () => {
|
|
92
|
+
expect(typeof NodeFetcher.json).toBe('function');
|
|
93
|
+
});
|
|
94
|
+
test('应该有txt方法 (Should have txt method)', () => {
|
|
95
|
+
expect(typeof NodeFetcher.txt).toBe('function');
|
|
96
|
+
});
|
|
97
|
+
test('应该有markdown方法 (Should have markdown method)', () => {
|
|
98
|
+
expect(typeof NodeFetcher.markdown).toBe('function');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe('html方法测试 (html Method Tests)', () => {
|
|
102
|
+
test('应该能够获取HTML内容 (Should fetch HTML content)', async () => {
|
|
103
|
+
// 准备测试数据
|
|
104
|
+
const payload = {
|
|
105
|
+
url: 'https://example.com',
|
|
106
|
+
debug: true
|
|
107
|
+
};
|
|
108
|
+
// 调用方法
|
|
109
|
+
const result = await NodeFetcher.html(payload);
|
|
110
|
+
// 验证结果
|
|
111
|
+
expect(result).toBeDefined();
|
|
112
|
+
expect(result.html).toBeDefined();
|
|
113
|
+
expect(result.url).toBe('https://example.com');
|
|
114
|
+
expect(result.status).toBe(200);
|
|
115
|
+
expect(result.headers).toBeDefined();
|
|
116
|
+
expect(result.html).toContain('<html>');
|
|
117
|
+
// 验证fetch被调用
|
|
118
|
+
expect(fetch).toHaveBeenCalledWith(payload.url, expect.objectContaining({
|
|
119
|
+
headers: expect.any(Object),
|
|
120
|
+
timeout: expect.any(Number)
|
|
121
|
+
}));
|
|
122
|
+
});
|
|
123
|
+
test('应该处理获取HTML时的错误 (Should handle errors when fetching HTML)', async () => {
|
|
124
|
+
// 模拟fetch失败
|
|
125
|
+
fetch.mockRejectedValueOnce(new Error('Network error'));
|
|
126
|
+
// 准备测试数据
|
|
127
|
+
const payload = {
|
|
128
|
+
url: 'https://example.com',
|
|
129
|
+
debug: true
|
|
130
|
+
};
|
|
131
|
+
// 调用方法并验证它抛出错误
|
|
132
|
+
await expect(NodeFetcher.html(payload)).rejects.toThrow('Network error');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe('json方法测试 (json Method Tests)', () => {
|
|
136
|
+
test('应该能够获取JSON内容 (Should fetch JSON content)', async () => {
|
|
137
|
+
// 准备测试数据
|
|
138
|
+
const payload = {
|
|
139
|
+
url: 'https://example.com/api',
|
|
140
|
+
debug: true
|
|
141
|
+
};
|
|
142
|
+
// 调用方法
|
|
143
|
+
const result = await NodeFetcher.json(payload);
|
|
144
|
+
// 验证结果
|
|
145
|
+
expect(result).toBeDefined();
|
|
146
|
+
expect(result.json).toBeDefined();
|
|
147
|
+
expect(result.json).toEqual({ test: 'data' });
|
|
148
|
+
expect(result.url).toBe('https://example.com/api');
|
|
149
|
+
expect(result.status).toBe(200);
|
|
150
|
+
expect(result.headers).toBeDefined();
|
|
151
|
+
// 验证fetch被调用
|
|
152
|
+
expect(fetch).toHaveBeenCalledWith(payload.url, expect.objectContaining({
|
|
153
|
+
headers: expect.any(Object),
|
|
154
|
+
timeout: expect.any(Number)
|
|
155
|
+
}));
|
|
156
|
+
});
|
|
157
|
+
test('应该处理获取JSON时的错误 (Should handle errors when fetching JSON)', async () => {
|
|
158
|
+
// 模拟fetch失败
|
|
159
|
+
fetch.mockRejectedValueOnce(new Error('Network error'));
|
|
160
|
+
// 准备测试数据
|
|
161
|
+
const payload = {
|
|
162
|
+
url: 'https://example.com/api',
|
|
163
|
+
debug: true
|
|
164
|
+
};
|
|
165
|
+
// 调用方法并验证它抛出错误
|
|
166
|
+
await expect(NodeFetcher.json(payload)).rejects.toThrow('Network error');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
describe('txt方法测试 (txt Method Tests)', () => {
|
|
170
|
+
test('应该能够获取文本内容 (Should fetch text content)', async () => {
|
|
171
|
+
// 准备测试数据
|
|
172
|
+
const payload = {
|
|
173
|
+
url: 'https://example.com/text',
|
|
174
|
+
debug: true
|
|
175
|
+
};
|
|
176
|
+
// 调用方法
|
|
177
|
+
const result = await NodeFetcher.txt(payload);
|
|
178
|
+
// 验证结果
|
|
179
|
+
expect(result).toBeDefined();
|
|
180
|
+
expect(result.text).toBeDefined();
|
|
181
|
+
expect(result.url).toBe('https://example.com/text');
|
|
182
|
+
expect(result.status).toBe(200);
|
|
183
|
+
expect(result.headers).toBeDefined();
|
|
184
|
+
// 验证fetch被调用
|
|
185
|
+
expect(fetch).toHaveBeenCalledWith(payload.url, expect.objectContaining({
|
|
186
|
+
headers: expect.any(Object),
|
|
187
|
+
timeout: expect.any(Number)
|
|
188
|
+
}));
|
|
189
|
+
});
|
|
190
|
+
test('应该处理获取文本时的错误 (Should handle errors when fetching text)', async () => {
|
|
191
|
+
// 模拟fetch失败
|
|
192
|
+
fetch.mockRejectedValueOnce(new Error('Network error'));
|
|
193
|
+
// 准备测试数据
|
|
194
|
+
const payload = {
|
|
195
|
+
url: 'https://example.com/text',
|
|
196
|
+
debug: true
|
|
197
|
+
};
|
|
198
|
+
// 调用方法并验证它抛出错误
|
|
199
|
+
await expect(NodeFetcher.txt(payload)).rejects.toThrow('Network error');
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
describe('markdown方法测试 (markdown Method Tests)', () => {
|
|
203
|
+
test('应该能够获取Markdown内容 (Should fetch Markdown content)', async () => {
|
|
204
|
+
// 准备测试数据
|
|
205
|
+
const payload = {
|
|
206
|
+
url: 'https://example.com/md',
|
|
207
|
+
debug: true
|
|
208
|
+
};
|
|
209
|
+
// 调用方法
|
|
210
|
+
const result = await NodeFetcher.markdown(payload);
|
|
211
|
+
// 验证结果
|
|
212
|
+
expect(result).toBeDefined();
|
|
213
|
+
expect(result.markdown).toBeDefined();
|
|
214
|
+
expect(result.url).toBe('https://example.com/md');
|
|
215
|
+
expect(result.status).toBe(200);
|
|
216
|
+
expect(result.headers).toBeDefined();
|
|
217
|
+
// 验证fetch被调用
|
|
218
|
+
expect(fetch).toHaveBeenCalledWith(payload.url, expect.objectContaining({
|
|
219
|
+
headers: expect.any(Object),
|
|
220
|
+
timeout: expect.any(Number)
|
|
221
|
+
}));
|
|
222
|
+
});
|
|
223
|
+
test('应该处理获取Markdown时的错误 (Should handle errors when fetching Markdown)', async () => {
|
|
224
|
+
// 模拟fetch失败
|
|
225
|
+
fetch.mockRejectedValueOnce(new Error('Network error'));
|
|
226
|
+
// 准备测试数据
|
|
227
|
+
const payload = {
|
|
228
|
+
url: 'https://example.com/md',
|
|
229
|
+
debug: true
|
|
230
|
+
};
|
|
231
|
+
// 调用方法并验证它抛出错误
|
|
232
|
+
await expect(NodeFetcher.markdown(payload)).rejects.toThrow('Network error');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
describe('代理功能测试 (Proxy Functionality Tests)', () => {
|
|
236
|
+
test('应该正确处理HTTP代理 (Should handle HTTP proxy correctly)', async () => {
|
|
237
|
+
// 准备测试数据
|
|
238
|
+
const payload = {
|
|
239
|
+
url: 'http://example.com',
|
|
240
|
+
proxy: 'http://proxy.example.com:8080',
|
|
241
|
+
debug: true
|
|
242
|
+
};
|
|
243
|
+
// 调用方法
|
|
244
|
+
await NodeFetcher.html(payload);
|
|
245
|
+
// 验证HttpProxyAgent被调用
|
|
246
|
+
const { HttpProxyAgent } = require('http-proxy-agent');
|
|
247
|
+
expect(HttpProxyAgent).toHaveBeenCalledWith(payload.proxy);
|
|
248
|
+
});
|
|
249
|
+
test('应该正确处理HTTPS代理 (Should handle HTTPS proxy correctly)', async () => {
|
|
250
|
+
// 准备测试数据
|
|
251
|
+
const payload = {
|
|
252
|
+
url: 'https://example.com',
|
|
253
|
+
proxy: 'http://proxy.example.com:8080',
|
|
254
|
+
debug: true
|
|
255
|
+
};
|
|
256
|
+
// 调用方法
|
|
257
|
+
await NodeFetcher.html(payload);
|
|
258
|
+
// 验证HttpsProxyAgent被调用
|
|
259
|
+
const { HttpsProxyAgent } = require('https-proxy-agent');
|
|
260
|
+
expect(HttpsProxyAgent).toHaveBeenCalledWith(payload.proxy);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
});
|
|
@@ -0,0 +1,250 @@
|
|
|
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 { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
7
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
8
|
+
// 保存原始值以便在测试后恢复
|
|
9
|
+
const originalArgv = process.argv;
|
|
10
|
+
const originalExit = process.exit;
|
|
11
|
+
const originalStdoutWrite = process.stdout.write;
|
|
12
|
+
// 模拟 logger 模块
|
|
13
|
+
const mockLog = jest.fn();
|
|
14
|
+
jest.mock('../src/lib/logger.js', () => ({
|
|
15
|
+
log: mockLog,
|
|
16
|
+
COMPONENTS: {
|
|
17
|
+
CLIENT: 'CLIENT',
|
|
18
|
+
SERVER: 'SERVER',
|
|
19
|
+
NODE_FETCH: 'NODE-FETCH',
|
|
20
|
+
BROWSER_FETCH: 'BROWSER-FETCH'
|
|
21
|
+
}
|
|
22
|
+
}));
|
|
23
|
+
// 模拟依赖模块
|
|
24
|
+
jest.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
|
|
25
|
+
Client: jest.fn().mockImplementation(() => ({
|
|
26
|
+
connect: jest.fn().mockResolvedValue(undefined),
|
|
27
|
+
callTool: jest.fn(),
|
|
28
|
+
close: jest.fn().mockResolvedValue(undefined)
|
|
29
|
+
}))
|
|
30
|
+
}));
|
|
31
|
+
jest.mock('@modelcontextprotocol/sdk/client/stdio.js', () => ({
|
|
32
|
+
StdioClientTransport: jest.fn().mockImplementation(() => ({}))
|
|
33
|
+
}));
|
|
34
|
+
jest.mock('child_process', () => ({
|
|
35
|
+
execSync: jest.fn()
|
|
36
|
+
}));
|
|
37
|
+
// 模拟 process.exit
|
|
38
|
+
jest.spyOn(process, 'exit').mockImplementation((code) => {
|
|
39
|
+
console.log(`[模拟] process.exit(${code})`);
|
|
40
|
+
return undefined;
|
|
41
|
+
});
|
|
42
|
+
// 创建模拟的客户端模块
|
|
43
|
+
const mockClientModule = {
|
|
44
|
+
responseRequiresBrowser: jest.fn((response, debug = false) => {
|
|
45
|
+
if (response.isError) {
|
|
46
|
+
const errorText = response.content[0].text.toLowerCase();
|
|
47
|
+
if (debug) {
|
|
48
|
+
mockLog('fetcher.fetchError', debug, { error: response.content[0].text }, 'CLIENT');
|
|
49
|
+
if (errorText.includes('403') || errorText.includes('forbidden')) {
|
|
50
|
+
mockLog('errors.forbidden', debug, {}, 'CLIENT');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return errorText.includes('403') ||
|
|
54
|
+
errorText.includes('forbidden') ||
|
|
55
|
+
errorText.includes('cloudflare') ||
|
|
56
|
+
errorText.includes('timeout') ||
|
|
57
|
+
errorText.includes('econnrefused');
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}),
|
|
61
|
+
smartFetch: jest.fn(async (params) => {
|
|
62
|
+
const mockClient = new Client({
|
|
63
|
+
name: "fetch-mcp-client",
|
|
64
|
+
version: "1.0.0"
|
|
65
|
+
});
|
|
66
|
+
await mockClient.connect(new StdioClientTransport({
|
|
67
|
+
command: 'node',
|
|
68
|
+
args: ['index.js'],
|
|
69
|
+
stderr: 'inherit'
|
|
70
|
+
}));
|
|
71
|
+
try {
|
|
72
|
+
// 使用 any 类型来避免类型错误
|
|
73
|
+
const result = await mockClient.callTool({
|
|
74
|
+
name: params.method || 'fetch_html',
|
|
75
|
+
arguments: params
|
|
76
|
+
});
|
|
77
|
+
// 确保返回值符合 ResponseType 接口
|
|
78
|
+
return {
|
|
79
|
+
isError: typeof result.isError === 'boolean' ? result.isError : false,
|
|
80
|
+
content: Array.isArray(result.content) ? result.content : [{ text: '' }]
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
await mockClient.close();
|
|
85
|
+
}
|
|
86
|
+
}),
|
|
87
|
+
main: jest.fn(() => {
|
|
88
|
+
// 检查命令行参数
|
|
89
|
+
if (process.argv.length < 4) {
|
|
90
|
+
mockLog('client.usageInfo', true, {}, 'CLIENT');
|
|
91
|
+
mockLog('client.exampleUsage', true, {}, 'CLIENT');
|
|
92
|
+
process.exit(1);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const method = process.argv[2];
|
|
96
|
+
let paramsJson = process.argv[3];
|
|
97
|
+
try {
|
|
98
|
+
JSON.parse(paramsJson);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
mockLog('client.invalidJson', true, {}, 'CLIENT');
|
|
102
|
+
process.exit(1);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// 模拟成功执行
|
|
106
|
+
return;
|
|
107
|
+
})
|
|
108
|
+
};
|
|
109
|
+
// 模拟 client.ts 模块
|
|
110
|
+
jest.mock('../src/client.js', () => mockClientModule, { virtual: true });
|
|
111
|
+
describe('client.ts 测试', () => {
|
|
112
|
+
// 在每个测试前重置模拟
|
|
113
|
+
beforeEach(() => {
|
|
114
|
+
jest.resetModules();
|
|
115
|
+
jest.clearAllMocks();
|
|
116
|
+
});
|
|
117
|
+
// 在所有测试后恢复原始值
|
|
118
|
+
afterAll(() => {
|
|
119
|
+
process.exit = originalExit;
|
|
120
|
+
process.argv = originalArgv;
|
|
121
|
+
});
|
|
122
|
+
describe('responseRequiresBrowser 函数测试', () => {
|
|
123
|
+
test('应该在响应包含 403 错误时返回 true', () => {
|
|
124
|
+
// 模拟依赖
|
|
125
|
+
jest.doMock('../src/lib/logger.js', () => ({
|
|
126
|
+
log: jest.fn(),
|
|
127
|
+
COMPONENTS: {
|
|
128
|
+
CLIENT: 'CLIENT'
|
|
129
|
+
}
|
|
130
|
+
}));
|
|
131
|
+
// 导入被测试的模块
|
|
132
|
+
const { responseRequiresBrowser } = require('../src/client.js');
|
|
133
|
+
const response = {
|
|
134
|
+
isError: true,
|
|
135
|
+
content: [{ text: '403 Forbidden' }]
|
|
136
|
+
};
|
|
137
|
+
const result = responseRequiresBrowser(response, true);
|
|
138
|
+
expect(result).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
test('应该在响应不是错误时返回 false', () => {
|
|
141
|
+
// 模拟依赖
|
|
142
|
+
jest.doMock('../src/lib/logger.js', () => ({
|
|
143
|
+
log: jest.fn(),
|
|
144
|
+
COMPONENTS: {
|
|
145
|
+
CLIENT: 'CLIENT'
|
|
146
|
+
}
|
|
147
|
+
}));
|
|
148
|
+
// 导入被测试的模块
|
|
149
|
+
const { responseRequiresBrowser } = require('../src/client.js');
|
|
150
|
+
const response = {
|
|
151
|
+
isError: false,
|
|
152
|
+
content: [{ text: 'Success' }]
|
|
153
|
+
};
|
|
154
|
+
const result = responseRequiresBrowser(response);
|
|
155
|
+
expect(result).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
describe('smartFetch 函数测试', () => {
|
|
159
|
+
test('应该使用标准模式发送请求并返回结果', async () => {
|
|
160
|
+
// 模拟成功响应
|
|
161
|
+
const mockResult = {
|
|
162
|
+
isError: false,
|
|
163
|
+
content: [{ text: '<html><body>Success</body></html>' }]
|
|
164
|
+
};
|
|
165
|
+
// 设置 callTool 的返回值
|
|
166
|
+
const mockCallTool = jest.fn().mockResolvedValue(mockResult);
|
|
167
|
+
Client.mockImplementation(() => ({
|
|
168
|
+
connect: jest.fn().mockResolvedValue(undefined),
|
|
169
|
+
callTool: mockCallTool,
|
|
170
|
+
close: jest.fn().mockResolvedValue(undefined)
|
|
171
|
+
}));
|
|
172
|
+
// 调用函数
|
|
173
|
+
const result = await mockClientModule.smartFetch({
|
|
174
|
+
url: 'https://example.com',
|
|
175
|
+
debug: true
|
|
176
|
+
});
|
|
177
|
+
// 验证结果
|
|
178
|
+
expect(result).toEqual(mockResult);
|
|
179
|
+
// 验证调用
|
|
180
|
+
expect(Client).toHaveBeenCalled();
|
|
181
|
+
expect(StdioClientTransport).toHaveBeenCalled();
|
|
182
|
+
expect(mockCallTool).toHaveBeenCalledWith({
|
|
183
|
+
name: 'fetch_html',
|
|
184
|
+
arguments: expect.objectContaining({
|
|
185
|
+
url: 'https://example.com',
|
|
186
|
+
debug: true
|
|
187
|
+
})
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
test('应该在任何情况下都关闭客户端连接', async () => {
|
|
191
|
+
// 模拟 callTool 抛出错误
|
|
192
|
+
const mockClose = jest.fn().mockResolvedValue(undefined);
|
|
193
|
+
Client.mockImplementation(() => ({
|
|
194
|
+
connect: jest.fn().mockResolvedValue(undefined),
|
|
195
|
+
callTool: jest.fn().mockRejectedValue(new Error('Test error')),
|
|
196
|
+
close: mockClose
|
|
197
|
+
}));
|
|
198
|
+
// 调用函数并捕获错误
|
|
199
|
+
await expect(mockClientModule.smartFetch({
|
|
200
|
+
url: 'https://example.com',
|
|
201
|
+
debug: true
|
|
202
|
+
})).rejects.toThrow('Test error');
|
|
203
|
+
// 验证 close 被调用
|
|
204
|
+
expect(mockClose).toHaveBeenCalled();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
describe('main 函数测试', () => {
|
|
208
|
+
test('应该在参数不足时显示使用信息并退出', () => {
|
|
209
|
+
// 保存原始的 process.argv
|
|
210
|
+
const testArgv = process.argv;
|
|
211
|
+
try {
|
|
212
|
+
// 设置参数不足
|
|
213
|
+
process.argv = ['node', 'client.js'];
|
|
214
|
+
// 重置 mockLog 函数
|
|
215
|
+
mockLog.mockClear();
|
|
216
|
+
// 调用 main 函数
|
|
217
|
+
mockClientModule.main();
|
|
218
|
+
// 验证 log 函数被调用
|
|
219
|
+
expect(mockLog).toHaveBeenCalledWith('client.usageInfo', true, {}, 'CLIENT');
|
|
220
|
+
expect(mockLog).toHaveBeenCalledWith('client.exampleUsage', true, {}, 'CLIENT');
|
|
221
|
+
// 验证 process.exit 被调用
|
|
222
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
// 恢复原始的 process.argv
|
|
226
|
+
process.argv = testArgv;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
test('应该在 JSON 解析失败时显示错误并退出', () => {
|
|
230
|
+
// 保存原始的 process.argv
|
|
231
|
+
const testArgv = process.argv;
|
|
232
|
+
try {
|
|
233
|
+
// 设置无效的 JSON
|
|
234
|
+
process.argv = ['node', 'client.js', 'fetch_html', 'invalid-json'];
|
|
235
|
+
// 重置 mockLog 函数
|
|
236
|
+
mockLog.mockClear();
|
|
237
|
+
// 调用 main 函数
|
|
238
|
+
mockClientModule.main();
|
|
239
|
+
// 验证 log 函数被调用
|
|
240
|
+
expect(mockLog).toHaveBeenCalledWith('client.invalidJson', true, {}, 'CLIENT');
|
|
241
|
+
// 验证 process.exit 被调用
|
|
242
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
// 恢复原始的 process.argv
|
|
246
|
+
process.argv = testArgv;
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
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 i18next, { t, changeLanguage, getCurrentLanguage } from '../src/lib/i18n/index.js';
|
|
7
|
+
import { createLogger } from '../src/lib/i18n/logger.js';
|
|
8
|
+
describe('i18n模块测试 (i18n Module Tests)', () => {
|
|
9
|
+
// 保存原始环境变量
|
|
10
|
+
const originalEnv = process.env.MCP_LANG;
|
|
11
|
+
// 在每个测试后恢复环境变量
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
process.env.MCP_LANG = originalEnv;
|
|
14
|
+
});
|
|
15
|
+
test('默认语言为英语 (Default language is English)', () => {
|
|
16
|
+
// 确保没有设置MCP_LANG环境变量
|
|
17
|
+
delete process.env.MCP_LANG;
|
|
18
|
+
// 重新初始化i18next
|
|
19
|
+
i18next.init({
|
|
20
|
+
resources: i18next.options.resources,
|
|
21
|
+
lng: process.env.MCP_LANG || 'en',
|
|
22
|
+
fallbackLng: 'en',
|
|
23
|
+
interpolation: {
|
|
24
|
+
escapeValue: false
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
// 验证当前语言为英语
|
|
28
|
+
expect(getCurrentLanguage()).toBe('en');
|
|
29
|
+
});
|
|
30
|
+
test('可以切换语言 (Can switch language)', () => {
|
|
31
|
+
// 切换到中文
|
|
32
|
+
changeLanguage('zh');
|
|
33
|
+
// 验证当前语言为中文
|
|
34
|
+
expect(getCurrentLanguage()).toBe('zh');
|
|
35
|
+
// 切换回英语
|
|
36
|
+
changeLanguage('en');
|
|
37
|
+
// 验证当前语言为英语
|
|
38
|
+
expect(getCurrentLanguage()).toBe('en');
|
|
39
|
+
});
|
|
40
|
+
test('翻译函数能正确工作 (Translation function works correctly)', () => {
|
|
41
|
+
// 切换到英语
|
|
42
|
+
changeLanguage('en');
|
|
43
|
+
// 测试一个存在的翻译键
|
|
44
|
+
const serverStartingKey = 'server.starting';
|
|
45
|
+
const serverStartingTranslation = t(serverStartingKey);
|
|
46
|
+
// 验证翻译结果不为空且不等于键名
|
|
47
|
+
expect(serverStartingTranslation).not.toBe('');
|
|
48
|
+
expect(serverStartingTranslation).not.toBe(serverStartingKey);
|
|
49
|
+
// 切换到中文
|
|
50
|
+
changeLanguage('zh');
|
|
51
|
+
// 测试同一个翻译键
|
|
52
|
+
const zhTranslation = t(serverStartingKey);
|
|
53
|
+
// 验证中文翻译结果不为空且不等于键名
|
|
54
|
+
expect(zhTranslation).not.toBe('');
|
|
55
|
+
expect(zhTranslation).not.toBe(serverStartingKey);
|
|
56
|
+
// 验证中文翻译与英文翻译不同
|
|
57
|
+
expect(zhTranslation).not.toBe(serverStartingTranslation);
|
|
58
|
+
});
|
|
59
|
+
test('带参数的翻译 (Translation with parameters)', () => {
|
|
60
|
+
// 切换到英语
|
|
61
|
+
changeLanguage('en');
|
|
62
|
+
// 使用一个简单的翻译键进行测试
|
|
63
|
+
const testKey = 'server.starting';
|
|
64
|
+
const params = { name: 'Test Server' };
|
|
65
|
+
const translationWithParams = t(testKey, params);
|
|
66
|
+
// 验证翻译结果不为空且不等于键名
|
|
67
|
+
expect(translationWithParams).not.toBe('');
|
|
68
|
+
expect(translationWithParams).not.toBe(testKey);
|
|
69
|
+
});
|
|
70
|
+
test('不存在的翻译键返回键名 (Non-existent translation key returns key name)', () => {
|
|
71
|
+
// 测试不存在的翻译键
|
|
72
|
+
const nonExistentKey = 'non.existent.key';
|
|
73
|
+
const translation = t(nonExistentKey);
|
|
74
|
+
// 验证翻译结果等于键名
|
|
75
|
+
expect(translation).toBe(nonExistentKey);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('i18n Logger模块测试 (i18n Logger Module Tests)', () => {
|
|
79
|
+
// 保存原始的console方法
|
|
80
|
+
const originalConsoleLog = console.log;
|
|
81
|
+
const originalConsoleError = console.error;
|
|
82
|
+
// 在每个测试前设置mock
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
console.log = jest.fn();
|
|
85
|
+
console.error = jest.fn();
|
|
86
|
+
process.env.DEBUG = 'false';
|
|
87
|
+
});
|
|
88
|
+
// 在每个测试后恢复原始方法
|
|
89
|
+
afterEach(() => {
|
|
90
|
+
console.log = originalConsoleLog;
|
|
91
|
+
console.error = originalConsoleError;
|
|
92
|
+
delete process.env.DEBUG;
|
|
93
|
+
});
|
|
94
|
+
test('创建日志记录器 (Create logger)', () => {
|
|
95
|
+
// 创建日志记录器
|
|
96
|
+
const logger = createLogger('TEST');
|
|
97
|
+
// 验证logger对象包含预期的方法
|
|
98
|
+
expect(logger).toHaveProperty('info');
|
|
99
|
+
expect(logger).toHaveProperty('error');
|
|
100
|
+
expect(logger).toHaveProperty('warn');
|
|
101
|
+
expect(logger).toHaveProperty('debug');
|
|
102
|
+
});
|
|
103
|
+
test('info方法在DEBUG=true时输出日志 (info method outputs logs when DEBUG=true)', () => {
|
|
104
|
+
// 设置DEBUG环境变量为true
|
|
105
|
+
process.env.DEBUG = 'true';
|
|
106
|
+
// 创建日志记录器
|
|
107
|
+
const logger = createLogger('TEST');
|
|
108
|
+
// 调用info方法
|
|
109
|
+
logger.info('test.info');
|
|
110
|
+
// 验证console.error被调用(i18n logger使用console.error输出所有日志)
|
|
111
|
+
expect(console.error).toHaveBeenCalled();
|
|
112
|
+
// 验证调用参数包含前缀和消息
|
|
113
|
+
const callArg = console.error.mock.calls[0][0];
|
|
114
|
+
expect(callArg).toContain('[TEST]');
|
|
115
|
+
});
|
|
116
|
+
test('info方法在DEBUG=false且forceDebug=false时不输出日志 (info method does not output logs when DEBUG=false and forceDebug=false)', () => {
|
|
117
|
+
// 创建日志记录器
|
|
118
|
+
const logger = createLogger('TEST');
|
|
119
|
+
// 调用info方法,不强制输出
|
|
120
|
+
logger.info('test.info');
|
|
121
|
+
// 验证console.error没有被调用
|
|
122
|
+
expect(console.error).not.toHaveBeenCalled();
|
|
123
|
+
});
|
|
124
|
+
test('info方法在forceDebug=true时输出日志 (info method outputs logs when forceDebug=true)', () => {
|
|
125
|
+
// 创建日志记录器
|
|
126
|
+
const logger = createLogger('TEST');
|
|
127
|
+
// 调用info方法,强制输出
|
|
128
|
+
logger.info('test.info', {}, true);
|
|
129
|
+
// 验证console.error被调用
|
|
130
|
+
expect(console.error).toHaveBeenCalled();
|
|
131
|
+
// 验证调用参数包含前缀和消息
|
|
132
|
+
const callArg = console.error.mock.calls[0][0];
|
|
133
|
+
expect(callArg).toContain('[TEST]');
|
|
134
|
+
});
|
|
135
|
+
test('error方法总是输出日志 (error method always outputs logs)', () => {
|
|
136
|
+
// 创建日志记录器
|
|
137
|
+
const logger = createLogger('TEST');
|
|
138
|
+
// 调用error方法
|
|
139
|
+
logger.error('test.error');
|
|
140
|
+
// 验证console.error被调用
|
|
141
|
+
expect(console.error).toHaveBeenCalled();
|
|
142
|
+
// 验证调用参数包含前缀和消息
|
|
143
|
+
const callArg = console.error.mock.calls[0][0];
|
|
144
|
+
expect(callArg).toContain('[TEST]');
|
|
145
|
+
expect(callArg).toContain('test.error');
|
|
146
|
+
});
|
|
147
|
+
test('warn方法总是输出日志 (warn method always outputs logs)', () => {
|
|
148
|
+
// 创建日志记录器
|
|
149
|
+
const logger = createLogger('TEST');
|
|
150
|
+
// 调用warn方法
|
|
151
|
+
logger.warn('test.warn');
|
|
152
|
+
// 验证console.error被调用
|
|
153
|
+
expect(console.error).toHaveBeenCalled();
|
|
154
|
+
// 验证调用参数包含前缀和消息
|
|
155
|
+
const callArg = console.error.mock.calls[0][0];
|
|
156
|
+
expect(callArg).toContain('[TEST]');
|
|
157
|
+
expect(callArg).toContain('test.warn');
|
|
158
|
+
});
|
|
159
|
+
test('debug方法在forceDebug=true时输出日志 (debug method outputs logs when forceDebug=true)', () => {
|
|
160
|
+
// 创建日志记录器
|
|
161
|
+
const logger = createLogger('TEST');
|
|
162
|
+
// 调用debug方法,强制输出
|
|
163
|
+
logger.debug('test.debug', {}, true);
|
|
164
|
+
// 验证console.error被调用
|
|
165
|
+
expect(console.error).toHaveBeenCalled();
|
|
166
|
+
// 验证调用参数包含前缀和消息
|
|
167
|
+
const callArg = console.error.mock.calls[0][0];
|
|
168
|
+
expect(callArg).toContain('[TEST]');
|
|
169
|
+
expect(callArg).toContain('test.debug');
|
|
170
|
+
});
|
|
171
|
+
test('debug方法在forceDebug=false时不输出日志 (debug method does not output logs when forceDebug=false)', () => {
|
|
172
|
+
// 创建日志记录器
|
|
173
|
+
const logger = createLogger('TEST');
|
|
174
|
+
// 调用debug方法,不强制输出
|
|
175
|
+
logger.debug('test.debug');
|
|
176
|
+
// 验证console.error没有被调用
|
|
177
|
+
expect(console.error).not.toHaveBeenCalled();
|
|
178
|
+
});
|
|
179
|
+
});
|