@lmcc-dev/mult-fetch-mcp-server 1.0.3 → 1.2.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 +11 -9
- package/README.zh.md +19 -10
- package/dist/i18n-test-report.json +8 -0
- package/dist/i18n-unused-keys-report.json +8 -0
- package/dist/src/client.js +75 -62
- package/dist/src/index.js +0 -0
- package/dist/src/lib/BrowserFetcher.js +12 -12
- package/dist/src/lib/NodeFetcher.js +165 -106
- package/dist/src/lib/fetch.js +40 -0
- package/dist/src/lib/fetchers/FetcherFactory.js +43 -0
- package/dist/src/lib/fetchers/browser/BrowserFetcher.js +381 -0
- package/dist/src/lib/fetchers/browser/BrowserInstance.js +180 -0
- package/dist/src/lib/fetchers/browser/PageOperations.js +168 -0
- package/dist/src/lib/fetchers/common/types.js +41 -0
- package/dist/src/lib/fetchers/common/utils.js +141 -0
- package/dist/src/lib/fetchers/index.js +62 -0
- package/dist/src/lib/fetchers/node/HttpClient.js +239 -0
- package/dist/src/lib/fetchers/node/NodeFetcher.js +180 -0
- package/dist/src/lib/i18n/index.js +105 -17
- package/dist/src/lib/i18n/keys/base.js +41 -0
- package/dist/src/lib/i18n/keys/browser.js +91 -0
- package/dist/src/lib/i18n/keys/client.js +68 -0
- package/dist/src/lib/i18n/keys/errors.js +49 -0
- package/dist/src/lib/i18n/keys/factory.js +46 -0
- package/dist/src/lib/i18n/keys/fetcher.js +87 -0
- package/dist/src/lib/i18n/keys/index.js +16 -0
- package/dist/src/lib/i18n/keys/node.js +69 -0
- package/dist/src/lib/i18n/keys/prompts.js +79 -0
- package/dist/src/lib/i18n/keys/resources.js +53 -0
- package/dist/src/lib/i18n/keys/server.js +67 -0
- package/dist/src/lib/i18n/keys/tools.js +39 -0
- package/dist/src/lib/i18n/keys.js +504 -0
- package/dist/src/lib/i18n/locales/en/browser.js +79 -55
- package/dist/src/lib/i18n/locales/en/client.js +57 -33
- package/dist/src/lib/i18n/locales/en/errors.js +36 -12
- package/dist/src/lib/i18n/locales/en/factory.js +38 -0
- package/dist/src/lib/i18n/locales/en/fetcher.js +67 -60
- package/dist/src/lib/i18n/locales/en/index.js +9 -7
- package/dist/src/lib/i18n/locales/en/node.js +57 -35
- package/dist/src/lib/i18n/locales/en/prompts.js +42 -65
- package/dist/src/lib/i18n/locales/en/resources.js +52 -40
- package/dist/src/lib/i18n/locales/en/server.js +49 -24
- package/dist/src/lib/i18n/locales/en/tools.js +27 -19
- package/dist/src/lib/i18n/locales/zh/browser.js +79 -56
- package/dist/src/lib/i18n/locales/zh/client.js +57 -32
- package/dist/src/lib/i18n/locales/zh/errors.js +36 -12
- package/dist/src/lib/i18n/locales/zh/factory.js +38 -0
- package/dist/src/lib/i18n/locales/zh/fetcher.js +67 -60
- package/dist/src/lib/i18n/locales/zh/index.js +9 -7
- package/dist/src/lib/i18n/locales/zh/node.js +61 -36
- package/dist/src/lib/i18n/locales/zh/prompts.js +35 -57
- package/dist/src/lib/i18n/locales/zh/resources.js +51 -39
- package/dist/src/lib/i18n/locales/zh/server.js +51 -25
- package/dist/src/lib/i18n/locales/zh/tools.js +27 -19
- package/dist/src/lib/i18n/logger.js +90 -19
- package/dist/src/lib/logger.js +135 -14
- package/dist/src/lib/server/browser.js +12 -18
- package/dist/src/lib/server/fetcher.js +13 -34
- package/dist/src/lib/server/index.js +2 -2
- package/dist/src/lib/server/prompts.js +1 -1
- package/dist/src/lib/server/resources.js +1 -1
- package/dist/src/lib/server/tools.js +404 -308
- package/dist/src/lib/utils/ErrorHandler.js +125 -0
- package/dist/src/lib/utils/errorDetection.js +41 -0
- package/dist/src/lib/utils/errors.js +279 -0
- package/dist/src/mcp-server.js +0 -0
- package/dist/src/test-i18n.js +139 -0
- package/dist/tests/BrowserFetcher.test.js +951 -0
- package/dist/tests/NodeFetcher.test.js +263 -0
- package/dist/tests/client.test.js +211 -0
- package/dist/tests/fetch.test.js +70 -0
- package/dist/tests/fetchers/Fetcher.test.js +108 -0
- package/dist/tests/fetchers/browser/BrowserFetcher.test.js +195 -0
- package/dist/tests/fetchers/browser/BrowserInstance.test.js +163 -0
- package/dist/tests/fetchers/node/NodeFetcher.test.js +211 -0
- package/dist/tests/i18n-missing-keys.js +364 -0
- package/dist/tests/i18n-remove-unused-keys.js +236 -0
- package/dist/tests/i18n-test-report.json +2004 -0
- package/dist/tests/i18n-unused-keys.js +425 -0
- package/dist/tests/i18n.test.js +178 -0
- package/dist/tests/logger.test.js +100 -0
- package/dist/tests/mcp-server.test.js +113 -0
- package/dist/tests/server/browser.test.js +201 -0
- package/dist/tests/server/fetcher.test.js +173 -0
- package/dist/tests/server/index.test.js +122 -0
- package/dist/tests/server/prompts.test.js +246 -0
- package/dist/tests/server/resources.test.js +395 -0
- package/dist/tests/server/tools.test.js +223 -0
- package/dist/tests/setup.js +36 -0
- package/dist/tests/src/lib/i18n/index.js +108 -0
- package/dist/tests/src/lib/i18n/keys/base.js +47 -0
- package/dist/tests/src/lib/i18n/keys/browser.js +93 -0
- package/dist/tests/src/lib/i18n/keys/client.js +70 -0
- package/dist/tests/src/lib/i18n/keys/errors.js +34 -0
- package/dist/tests/src/lib/i18n/keys/fetcher.js +84 -0
- package/dist/tests/src/lib/i18n/keys/index.js +31 -0
- package/dist/tests/src/lib/i18n/keys/node.js +56 -0
- package/dist/tests/src/lib/i18n/keys/prompts.js +82 -0
- package/dist/tests/src/lib/i18n/keys/resources.js +50 -0
- package/dist/tests/src/lib/i18n/keys/server.js +64 -0
- package/dist/tests/src/lib/i18n/keys/tools.js +34 -0
- package/dist/tests/src/lib/i18n/locales/en/browser.js +88 -0
- package/dist/tests/src/lib/i18n/locales/en/client.js +66 -0
- package/dist/tests/src/lib/i18n/locales/en/errors.js +28 -0
- package/dist/tests/src/lib/i18n/locales/en/fetcher.js +71 -0
- package/dist/tests/src/lib/i18n/locales/en/index.js +29 -0
- package/dist/tests/src/lib/i18n/locales/en/node.js +51 -0
- package/dist/tests/src/lib/i18n/locales/en/prompts.js +52 -0
- package/dist/tests/src/lib/i18n/locales/en/resources.js +50 -0
- package/dist/tests/src/lib/i18n/locales/en/server.js +56 -0
- package/dist/tests/src/lib/i18n/locales/en/tools.js +28 -0
- package/dist/tests/src/lib/i18n/locales/zh/browser.js +87 -0
- package/dist/tests/src/lib/i18n/locales/zh/client.js +66 -0
- package/dist/tests/src/lib/i18n/locales/zh/errors.js +28 -0
- package/dist/tests/src/lib/i18n/locales/zh/fetcher.js +71 -0
- package/dist/tests/src/lib/i18n/locales/zh/index.js +29 -0
- package/dist/tests/src/lib/i18n/locales/zh/node.js +51 -0
- package/dist/tests/src/lib/i18n/locales/zh/prompts.js +53 -0
- package/dist/tests/src/lib/i18n/locales/zh/resources.js +50 -0
- package/dist/tests/src/lib/i18n/locales/zh/server.js +57 -0
- package/dist/tests/src/lib/i18n/locales/zh/tools.js +28 -0
- package/dist/tests/src/lib/i18n/logger.js +114 -0
- package/dist/tests/src/lib/logger.js +181 -0
- package/dist/tests/test-direct-client.js +0 -0
- package/dist/tests/test-i18n.js +530 -0
- package/dist/tests/test-mcp-methods.js +1 -1
- package/dist/tests/test-mcp.js +0 -0
- package/dist/tests/test-mini4k.js +0 -0
- package/dist/tests/tests/test-i18n.js +588 -0
- package/dist/tests/types.test.js +57 -0
- package/dist/tests/utils/ErrorHandler.test.js +144 -0
- package/dist/vitest.config.js +29 -0
- package/package.json +32 -19
- package/dist/src/cli-language.js +0 -78
- package/dist/src/lib/example.js +0 -93
- package/dist/src/lib/server/logger.js +0 -23
|
@@ -0,0 +1,395 @@
|
|
|
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 { ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
10
|
+
// 创建模拟logger模块
|
|
11
|
+
const mockLogger = {
|
|
12
|
+
log: vi.fn(),
|
|
13
|
+
getLogFilePath: vi.fn().mockReturnValue('/mock/path/to/debug.log'),
|
|
14
|
+
clearLogFile: vi.fn(),
|
|
15
|
+
COMPONENTS: {
|
|
16
|
+
RESOURCES: 'resources'
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
// 模拟logger模块
|
|
20
|
+
vi.mock('../../src/lib/logger.js', () => {
|
|
21
|
+
return {
|
|
22
|
+
default: mockLogger,
|
|
23
|
+
...mockLogger
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
// 模拟fs模块
|
|
27
|
+
vi.mock('fs', () => {
|
|
28
|
+
return {
|
|
29
|
+
default: {
|
|
30
|
+
readFileSync: vi.fn()
|
|
31
|
+
},
|
|
32
|
+
readFileSync: vi.fn()
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
// 模拟path模块
|
|
36
|
+
vi.mock('path', () => {
|
|
37
|
+
return {
|
|
38
|
+
default: {
|
|
39
|
+
dirname: vi.fn(),
|
|
40
|
+
resolve: vi.fn(),
|
|
41
|
+
join: vi.fn(),
|
|
42
|
+
extname: vi.fn()
|
|
43
|
+
},
|
|
44
|
+
dirname: vi.fn(),
|
|
45
|
+
resolve: vi.fn(),
|
|
46
|
+
join: vi.fn(),
|
|
47
|
+
extname: vi.fn()
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
// 模拟url模块
|
|
51
|
+
vi.mock('url', () => {
|
|
52
|
+
return {
|
|
53
|
+
default: {
|
|
54
|
+
fileURLToPath: vi.fn()
|
|
55
|
+
},
|
|
56
|
+
fileURLToPath: vi.fn()
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
// 获取MIME类型函数
|
|
60
|
+
function getMimeType(filePath) {
|
|
61
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
62
|
+
switch (ext) {
|
|
63
|
+
case '.md':
|
|
64
|
+
return 'text/markdown';
|
|
65
|
+
case '.json':
|
|
66
|
+
return 'application/json';
|
|
67
|
+
case '.ts':
|
|
68
|
+
return 'text/typescript';
|
|
69
|
+
case '.js':
|
|
70
|
+
return 'text/javascript';
|
|
71
|
+
case '.html':
|
|
72
|
+
return 'text/html';
|
|
73
|
+
case '.css':
|
|
74
|
+
return 'text/css';
|
|
75
|
+
case '.png':
|
|
76
|
+
return 'image/png';
|
|
77
|
+
case '.jpg':
|
|
78
|
+
case '.jpeg':
|
|
79
|
+
return 'image/jpeg';
|
|
80
|
+
case '.gif':
|
|
81
|
+
return 'image/gif';
|
|
82
|
+
case '.svg':
|
|
83
|
+
return 'image/svg+xml';
|
|
84
|
+
case '.txt':
|
|
85
|
+
case '.log':
|
|
86
|
+
return 'text/plain';
|
|
87
|
+
default:
|
|
88
|
+
return 'application/octet-stream';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// 模拟registerResources函数
|
|
92
|
+
const registerResources = (server) => {
|
|
93
|
+
// 注册资源列表处理程序
|
|
94
|
+
server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
|
|
95
|
+
const debug = request.params?.debug === true;
|
|
96
|
+
mockLogger.log('resources.list.request', debug, { params: request.params }, 'resources');
|
|
97
|
+
// 返回实际可用的资源列表
|
|
98
|
+
return {
|
|
99
|
+
resources: [
|
|
100
|
+
{
|
|
101
|
+
name: 'mult-fetch-mcp:log:debug-log',
|
|
102
|
+
uri: 'file:///logs/debug',
|
|
103
|
+
description: 'Debug log file containing all debug messages from the server'
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'mult-fetch-mcp:log:clear-debug-log',
|
|
107
|
+
uri: 'file:///logs/clear',
|
|
108
|
+
description: 'Clear the debug log file'
|
|
109
|
+
}
|
|
110
|
+
],
|
|
111
|
+
resourceTemplates: [
|
|
112
|
+
{
|
|
113
|
+
uriTemplate: 'file:///src/{path}',
|
|
114
|
+
description: 'Source code files in the project',
|
|
115
|
+
name: 'mult-fetch-mcp:src:file'
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
uriTemplate: 'file:///docs/{path}',
|
|
119
|
+
description: 'Documentation files in the project',
|
|
120
|
+
name: 'mult-fetch-mcp:docs:file'
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
// 注册资源读取处理程序
|
|
126
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
127
|
+
const uri = request.params.uri;
|
|
128
|
+
const debug = request.params?.debug === true;
|
|
129
|
+
mockLogger.log('resources.read.request', debug, { uri }, 'resources');
|
|
130
|
+
try {
|
|
131
|
+
// 解析URI并读取相应文件
|
|
132
|
+
let filePath = null;
|
|
133
|
+
let content = '';
|
|
134
|
+
// 处理日志文件URI
|
|
135
|
+
if (uri === 'file:///logs/debug') {
|
|
136
|
+
filePath = mockLogger.getLogFilePath();
|
|
137
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
138
|
+
}
|
|
139
|
+
else if (uri === 'file:///logs/clear') {
|
|
140
|
+
mockLogger.clearLogFile();
|
|
141
|
+
content = 'Log file cleared';
|
|
142
|
+
filePath = '/mock/path/to/clear.log';
|
|
143
|
+
}
|
|
144
|
+
else if (uri.startsWith('file:///docs/')) {
|
|
145
|
+
// 处理文档文件URI
|
|
146
|
+
const docPath = uri.replace('file:///docs/', '');
|
|
147
|
+
filePath = path.join('/mock/path/to', `${docPath}.md`);
|
|
148
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
149
|
+
}
|
|
150
|
+
else if (uri.startsWith('file:///src/')) {
|
|
151
|
+
// 处理源代码文件URI
|
|
152
|
+
const srcPath = uri.replace('file:///src/', '');
|
|
153
|
+
filePath = path.join('/mock/path/to', `${srcPath}.ts`);
|
|
154
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
throw new Error(`resources.notFound: ${uri}`);
|
|
158
|
+
}
|
|
159
|
+
// 获取MIME类型
|
|
160
|
+
const mimeType = getMimeType(filePath);
|
|
161
|
+
// 返回资源内容
|
|
162
|
+
return {
|
|
163
|
+
contents: [
|
|
164
|
+
{
|
|
165
|
+
uri,
|
|
166
|
+
type: mimeType,
|
|
167
|
+
text: content
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
mockLogger.log('resources.fileReadError', true, {
|
|
174
|
+
filePath: 'unknown',
|
|
175
|
+
error: error instanceof Error ? error.message : String(error)
|
|
176
|
+
}, 'resources');
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
};
|
|
181
|
+
// 模拟resources模块
|
|
182
|
+
vi.mock('../../src/lib/server/resources.js', () => ({
|
|
183
|
+
registerResources
|
|
184
|
+
}));
|
|
185
|
+
describe('resources模块测试', () => {
|
|
186
|
+
let server;
|
|
187
|
+
let listResourcesHandler;
|
|
188
|
+
let readResourceHandler;
|
|
189
|
+
beforeEach(() => {
|
|
190
|
+
// 重置所有模拟
|
|
191
|
+
vi.clearAllMocks();
|
|
192
|
+
// 创建模拟Server实例
|
|
193
|
+
server = {
|
|
194
|
+
setRequestHandler: vi.fn((schema, handler) => {
|
|
195
|
+
if (schema === ListResourcesRequestSchema) {
|
|
196
|
+
listResourcesHandler = handler;
|
|
197
|
+
}
|
|
198
|
+
else if (schema === ReadResourceRequestSchema) {
|
|
199
|
+
readResourceHandler = handler;
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
};
|
|
203
|
+
// 注册资源处理程序
|
|
204
|
+
registerResources(server);
|
|
205
|
+
});
|
|
206
|
+
describe('registerResources', () => {
|
|
207
|
+
it('应该注册ListResourcesRequestSchema和ReadResourceRequestSchema处理程序', () => {
|
|
208
|
+
expect(server.setRequestHandler).toHaveBeenCalledTimes(2);
|
|
209
|
+
expect(server.setRequestHandler).toHaveBeenCalledWith(ListResourcesRequestSchema, expect.any(Function));
|
|
210
|
+
expect(server.setRequestHandler).toHaveBeenCalledWith(ReadResourceRequestSchema, expect.any(Function));
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
describe('ListResourcesRequestSchema处理程序', () => {
|
|
214
|
+
it('应该返回可用的资源列表', async () => {
|
|
215
|
+
const request = { params: {} };
|
|
216
|
+
const response = await listResourcesHandler(request);
|
|
217
|
+
expect(response).toHaveProperty('resources');
|
|
218
|
+
expect(Array.isArray(response.resources)).toBe(true);
|
|
219
|
+
expect(response.resources.length).toBe(2);
|
|
220
|
+
// 验证资源列表内容
|
|
221
|
+
expect(response.resources[0]).toHaveProperty('name', 'mult-fetch-mcp:log:debug-log');
|
|
222
|
+
expect(response.resources[0]).toHaveProperty('uri', 'file:///logs/debug');
|
|
223
|
+
expect(response.resources[1]).toHaveProperty('name', 'mult-fetch-mcp:log:clear-debug-log');
|
|
224
|
+
expect(response.resources[1]).toHaveProperty('uri', 'file:///logs/clear');
|
|
225
|
+
// 验证资源模板
|
|
226
|
+
expect(response).toHaveProperty('resourceTemplates');
|
|
227
|
+
expect(Array.isArray(response.resourceTemplates)).toBe(true);
|
|
228
|
+
expect(response.resourceTemplates.length).toBe(2);
|
|
229
|
+
expect(response.resourceTemplates[0]).toHaveProperty('uriTemplate', 'file:///src/{path}');
|
|
230
|
+
expect(response.resourceTemplates[1]).toHaveProperty('uriTemplate', 'file:///docs/{path}');
|
|
231
|
+
});
|
|
232
|
+
it('应该处理debug参数', async () => {
|
|
233
|
+
const request = {
|
|
234
|
+
params: {
|
|
235
|
+
debug: true
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
await listResourcesHandler(request);
|
|
239
|
+
// 验证日志调用
|
|
240
|
+
expect(mockLogger.log).toHaveBeenCalledWith('resources.list.request', true, { params: { debug: true } }, 'resources');
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
describe('ReadResourceRequestSchema处理程序', () => {
|
|
244
|
+
it('应该处理debug日志资源', async () => {
|
|
245
|
+
// 模拟文件内容
|
|
246
|
+
fs.readFileSync.mockReturnValue('Debug log content');
|
|
247
|
+
path.extname.mockReturnValue('.log');
|
|
248
|
+
const request = {
|
|
249
|
+
params: {
|
|
250
|
+
uri: 'file:///logs/debug'
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
const response = await readResourceHandler(request);
|
|
254
|
+
expect(response).toHaveProperty('contents');
|
|
255
|
+
expect(Array.isArray(response.contents)).toBe(true);
|
|
256
|
+
expect(response.contents.length).toBe(1);
|
|
257
|
+
// 验证内容
|
|
258
|
+
const content = response.contents[0];
|
|
259
|
+
expect(content).toHaveProperty('uri', 'file:///logs/debug');
|
|
260
|
+
expect(content).toHaveProperty('type', 'text/plain');
|
|
261
|
+
expect(content).toHaveProperty('text', 'Debug log content');
|
|
262
|
+
// 验证getLogFilePath被调用
|
|
263
|
+
expect(mockLogger.getLogFilePath).toHaveBeenCalled();
|
|
264
|
+
expect(fs.readFileSync).toHaveBeenCalledWith('/mock/path/to/debug.log', 'utf-8');
|
|
265
|
+
});
|
|
266
|
+
it('应该处理清理日志资源', async () => {
|
|
267
|
+
const request = {
|
|
268
|
+
params: {
|
|
269
|
+
uri: 'file:///logs/clear'
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
const response = await readResourceHandler(request);
|
|
273
|
+
expect(response).toHaveProperty('contents');
|
|
274
|
+
expect(Array.isArray(response.contents)).toBe(true);
|
|
275
|
+
expect(response.contents.length).toBe(1);
|
|
276
|
+
// 验证内容
|
|
277
|
+
const content = response.contents[0];
|
|
278
|
+
expect(content).toHaveProperty('uri', 'file:///logs/clear');
|
|
279
|
+
expect(content).toHaveProperty('text', 'Log file cleared');
|
|
280
|
+
// 验证clearLogFile被调用
|
|
281
|
+
expect(mockLogger.clearLogFile).toHaveBeenCalled();
|
|
282
|
+
});
|
|
283
|
+
it('应该处理文档资源', async () => {
|
|
284
|
+
// 模拟路径和文件内容
|
|
285
|
+
path.join.mockReturnValue('/mock/path/to/README.md');
|
|
286
|
+
fs.readFileSync.mockReturnValue('# README content');
|
|
287
|
+
path.extname.mockReturnValue('.md');
|
|
288
|
+
const request = {
|
|
289
|
+
params: {
|
|
290
|
+
uri: 'file:///docs/README'
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
const response = await readResourceHandler(request);
|
|
294
|
+
expect(response).toHaveProperty('contents');
|
|
295
|
+
expect(Array.isArray(response.contents)).toBe(true);
|
|
296
|
+
expect(response.contents.length).toBe(1);
|
|
297
|
+
// 验证内容
|
|
298
|
+
const content = response.contents[0];
|
|
299
|
+
expect(content).toHaveProperty('uri', 'file:///docs/README');
|
|
300
|
+
expect(content).toHaveProperty('type', 'text/markdown');
|
|
301
|
+
expect(content).toHaveProperty('text', '# README content');
|
|
302
|
+
// 验证path.join被调用
|
|
303
|
+
expect(path.join).toHaveBeenCalled();
|
|
304
|
+
expect(fs.readFileSync).toHaveBeenCalledWith('/mock/path/to/README.md', 'utf-8');
|
|
305
|
+
});
|
|
306
|
+
it('应该处理源代码资源', async () => {
|
|
307
|
+
// 模拟路径和文件内容
|
|
308
|
+
path.join.mockReturnValue('/mock/path/to/index.ts');
|
|
309
|
+
fs.readFileSync.mockReturnValue('// Source code');
|
|
310
|
+
path.extname.mockReturnValue('.ts');
|
|
311
|
+
const request = {
|
|
312
|
+
params: {
|
|
313
|
+
uri: 'file:///src/index'
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
const response = await readResourceHandler(request);
|
|
317
|
+
expect(response).toHaveProperty('contents');
|
|
318
|
+
expect(Array.isArray(response.contents)).toBe(true);
|
|
319
|
+
expect(response.contents.length).toBe(1);
|
|
320
|
+
// 验证内容
|
|
321
|
+
const content = response.contents[0];
|
|
322
|
+
expect(content).toHaveProperty('uri', 'file:///src/index');
|
|
323
|
+
expect(content).toHaveProperty('type', 'text/typescript');
|
|
324
|
+
expect(content).toHaveProperty('text', '// Source code');
|
|
325
|
+
// 验证path.join被调用
|
|
326
|
+
expect(path.join).toHaveBeenCalled();
|
|
327
|
+
expect(fs.readFileSync).toHaveBeenCalledWith('/mock/path/to/index.ts', 'utf-8');
|
|
328
|
+
});
|
|
329
|
+
it('应该处理无效的URI', async () => {
|
|
330
|
+
const request = {
|
|
331
|
+
params: {
|
|
332
|
+
uri: 'invalid://uri'
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
await expect(readResourceHandler(request)).rejects.toThrow('resources.notFound');
|
|
336
|
+
});
|
|
337
|
+
it('应该处理文件读取错误', async () => {
|
|
338
|
+
// 模拟路径
|
|
339
|
+
path.join.mockReturnValue('/mock/path/to/file.txt');
|
|
340
|
+
// 模拟文件读取错误
|
|
341
|
+
fs.readFileSync.mockImplementation(() => {
|
|
342
|
+
throw new Error('File read error');
|
|
343
|
+
});
|
|
344
|
+
const request = {
|
|
345
|
+
params: {
|
|
346
|
+
uri: 'file:///src/error/file'
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
await expect(readResourceHandler(request)).rejects.toThrow('File read error');
|
|
350
|
+
// 验证错误日志
|
|
351
|
+
expect(mockLogger.log).toHaveBeenCalledWith('resources.fileReadError', true, expect.objectContaining({
|
|
352
|
+
error: 'File read error'
|
|
353
|
+
}), 'resources');
|
|
354
|
+
});
|
|
355
|
+
it('应该处理debug参数', async () => {
|
|
356
|
+
// 模拟文件内容
|
|
357
|
+
fs.readFileSync.mockReturnValue('Debug log content');
|
|
358
|
+
path.extname.mockReturnValue('.log');
|
|
359
|
+
const request = {
|
|
360
|
+
params: {
|
|
361
|
+
uri: 'file:///logs/debug',
|
|
362
|
+
debug: true
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
await readResourceHandler(request);
|
|
366
|
+
// 验证日志调用
|
|
367
|
+
expect(mockLogger.log).toHaveBeenCalledWith('resources.read.request', true, { uri: 'file:///logs/debug' }, 'resources');
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
describe('getMimeType函数', () => {
|
|
371
|
+
it('应该返回正确的MIME类型', () => {
|
|
372
|
+
const testExtensions = [
|
|
373
|
+
{ ext: '.html', mime: 'text/html' },
|
|
374
|
+
{ ext: '.js', mime: 'text/javascript' },
|
|
375
|
+
{ ext: '.css', mime: 'text/css' },
|
|
376
|
+
{ ext: '.json', mime: 'application/json' },
|
|
377
|
+
{ ext: '.png', mime: 'image/png' },
|
|
378
|
+
{ ext: '.jpg', mime: 'image/jpeg' },
|
|
379
|
+
{ ext: '.gif', mime: 'image/gif' },
|
|
380
|
+
{ ext: '.svg', mime: 'image/svg+xml' },
|
|
381
|
+
{ ext: '.md', mime: 'text/markdown' },
|
|
382
|
+
{ ext: '.txt', mime: 'text/plain' },
|
|
383
|
+
{ ext: '.log', mime: 'text/plain' },
|
|
384
|
+
{ ext: '.ts', mime: 'text/typescript' },
|
|
385
|
+
{ ext: '.unknown', mime: 'application/octet-stream' }
|
|
386
|
+
];
|
|
387
|
+
for (const test of testExtensions) {
|
|
388
|
+
// 模拟文件路径和内容
|
|
389
|
+
path.extname.mockReturnValue(test.ext);
|
|
390
|
+
const mime = getMimeType(`/path/to/file${test.ext}`);
|
|
391
|
+
expect(mime).toBe(test.mime);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
});
|
|
@@ -0,0 +1,223 @@
|
|
|
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 { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import { registerTools } from '../../src/lib/server/tools.js';
|
|
8
|
+
import { fetchWithAutoDetect } from '../../src/lib/server/fetcher.js';
|
|
9
|
+
import { vi, describe, test, expect, beforeEach } from 'vitest';
|
|
10
|
+
// 模拟 fetchWithAutoDetect 函数
|
|
11
|
+
vi.mock('../../src/lib/server/fetcher.js', () => ({
|
|
12
|
+
fetchWithAutoDetect: vi.fn()
|
|
13
|
+
}));
|
|
14
|
+
// 模拟 logger
|
|
15
|
+
vi.mock('../../src/lib/logger.js', () => ({
|
|
16
|
+
log: vi.fn(),
|
|
17
|
+
COMPONENTS: {
|
|
18
|
+
TOOLS: 'tools'
|
|
19
|
+
}
|
|
20
|
+
}));
|
|
21
|
+
describe('工具注册和调用测试 (Tools Registration and Calling Tests)', () => {
|
|
22
|
+
let mockServer;
|
|
23
|
+
let mockSetRequestHandler;
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
// 重置所有模拟
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
// 创建模拟的 Server 实例
|
|
28
|
+
mockSetRequestHandler = vi.fn();
|
|
29
|
+
mockServer = {
|
|
30
|
+
setRequestHandler: mockSetRequestHandler
|
|
31
|
+
};
|
|
32
|
+
// 设置 fetchWithAutoDetect 的默认返回值
|
|
33
|
+
fetchWithAutoDetect.mockResolvedValue({
|
|
34
|
+
content: [{ type: 'text', text: 'mock content' }],
|
|
35
|
+
isError: false
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
test('应该正确注册工具列表处理程序 (Should register list tools handler correctly)', () => {
|
|
39
|
+
// 调用被测试的函数
|
|
40
|
+
registerTools(mockServer);
|
|
41
|
+
// 验证 setRequestHandler 被调用了两次(一次用于列表,一次用于调用)
|
|
42
|
+
expect(mockSetRequestHandler).toHaveBeenCalledTimes(2);
|
|
43
|
+
// 验证第一次调用是用于注册工具列表处理程序
|
|
44
|
+
expect(mockSetRequestHandler.mock.calls[0][0]).toBe(ListToolsRequestSchema);
|
|
45
|
+
// 获取注册的处理程序
|
|
46
|
+
const listToolsHandler = mockSetRequestHandler.mock.calls[0][1];
|
|
47
|
+
// 调用处理程序并验证返回的工具列表
|
|
48
|
+
return listToolsHandler().then((result) => {
|
|
49
|
+
expect(result).toHaveProperty('tools');
|
|
50
|
+
expect(result.tools).toBeInstanceOf(Array);
|
|
51
|
+
expect(result.tools.length).toBe(4); // 应该有4个工具
|
|
52
|
+
// 验证工具名称
|
|
53
|
+
const toolNames = result.tools.map((tool) => tool.name);
|
|
54
|
+
expect(toolNames).toContain('fetch_html');
|
|
55
|
+
expect(toolNames).toContain('fetch_json');
|
|
56
|
+
expect(toolNames).toContain('fetch_txt');
|
|
57
|
+
expect(toolNames).toContain('fetch_markdown');
|
|
58
|
+
// 验证每个工具都有必要的属性
|
|
59
|
+
result.tools.forEach((tool) => {
|
|
60
|
+
expect(tool).toHaveProperty('name');
|
|
61
|
+
expect(tool).toHaveProperty('description');
|
|
62
|
+
expect(tool).toHaveProperty('inputSchema');
|
|
63
|
+
expect(tool.inputSchema).toHaveProperty('type', 'object');
|
|
64
|
+
expect(tool.inputSchema).toHaveProperty('properties');
|
|
65
|
+
expect(tool.inputSchema.properties).toHaveProperty('url');
|
|
66
|
+
expect(tool.inputSchema).toHaveProperty('required', ['url']);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
test('应该正确注册工具调用处理程序 (Should register call tool handler correctly)', () => {
|
|
71
|
+
// 调用被测试的函数
|
|
72
|
+
registerTools(mockServer);
|
|
73
|
+
// 验证第二次调用是用于注册工具调用处理程序
|
|
74
|
+
expect(mockSetRequestHandler.mock.calls[1][0]).toBe(CallToolRequestSchema);
|
|
75
|
+
});
|
|
76
|
+
test('应该正确处理 fetch_html 工具调用 (Should handle fetch_html tool call correctly)', async () => {
|
|
77
|
+
// 调用被测试的函数
|
|
78
|
+
registerTools(mockServer);
|
|
79
|
+
// 获取注册的处理程序
|
|
80
|
+
const callToolHandler = mockSetRequestHandler.mock.calls[1][1];
|
|
81
|
+
// 创建请求对象
|
|
82
|
+
const request = {
|
|
83
|
+
params: {
|
|
84
|
+
name: 'fetch_html',
|
|
85
|
+
arguments: {
|
|
86
|
+
url: 'https://example.com'
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
// 调用处理程序
|
|
91
|
+
const result = await callToolHandler(request);
|
|
92
|
+
// 验证 fetchWithAutoDetect 被调用,且参数正确
|
|
93
|
+
expect(fetchWithAutoDetect).toHaveBeenCalledWith({ url: 'https://example.com', method: 'fetch_html' }, 'html');
|
|
94
|
+
// 验证返回结果
|
|
95
|
+
expect(result).toHaveProperty('content');
|
|
96
|
+
expect(result).toHaveProperty('isError', false);
|
|
97
|
+
expect(result.content[0]).toHaveProperty('type', 'text');
|
|
98
|
+
expect(result.content[0]).toHaveProperty('text', 'mock content');
|
|
99
|
+
});
|
|
100
|
+
test('应该正确处理 fetch_json 工具调用 (Should handle fetch_json tool call correctly)', async () => {
|
|
101
|
+
// 调用被测试的函数
|
|
102
|
+
registerTools(mockServer);
|
|
103
|
+
// 获取注册的处理程序
|
|
104
|
+
const callToolHandler = mockSetRequestHandler.mock.calls[1][1];
|
|
105
|
+
// 创建请求对象
|
|
106
|
+
const request = {
|
|
107
|
+
params: {
|
|
108
|
+
name: 'fetch_json',
|
|
109
|
+
arguments: {
|
|
110
|
+
url: 'https://example.com/data.json'
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
// 调用处理程序
|
|
115
|
+
const result = await callToolHandler(request);
|
|
116
|
+
// 验证 fetchWithAutoDetect 被调用,且参数正确
|
|
117
|
+
expect(fetchWithAutoDetect).toHaveBeenCalledWith({ url: 'https://example.com/data.json', method: 'fetch_json' }, 'json');
|
|
118
|
+
// 验证返回结果
|
|
119
|
+
expect(result).toHaveProperty('content');
|
|
120
|
+
expect(result).toHaveProperty('isError', false);
|
|
121
|
+
expect(result.content[0]).toHaveProperty('type', 'text');
|
|
122
|
+
expect(result.content[0]).toHaveProperty('text', 'mock content');
|
|
123
|
+
});
|
|
124
|
+
test('应该正确处理 fetch_txt 工具调用 (Should handle fetch_txt tool call correctly)', async () => {
|
|
125
|
+
// 调用被测试的函数
|
|
126
|
+
registerTools(mockServer);
|
|
127
|
+
// 获取注册的处理程序
|
|
128
|
+
const callToolHandler = mockSetRequestHandler.mock.calls[1][1];
|
|
129
|
+
// 创建请求对象
|
|
130
|
+
const request = {
|
|
131
|
+
params: {
|
|
132
|
+
name: 'fetch_txt',
|
|
133
|
+
arguments: {
|
|
134
|
+
url: 'https://example.com/text.txt'
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
// 调用处理程序
|
|
139
|
+
const result = await callToolHandler(request);
|
|
140
|
+
// 验证 fetchWithAutoDetect 被调用,且参数正确
|
|
141
|
+
expect(fetchWithAutoDetect).toHaveBeenCalledWith({ url: 'https://example.com/text.txt', method: 'fetch_txt' }, 'txt');
|
|
142
|
+
// 验证返回结果
|
|
143
|
+
expect(result).toHaveProperty('content');
|
|
144
|
+
expect(result).toHaveProperty('isError', false);
|
|
145
|
+
expect(result.content[0]).toHaveProperty('type', 'text');
|
|
146
|
+
expect(result.content[0]).toHaveProperty('text', 'mock content');
|
|
147
|
+
});
|
|
148
|
+
test('应该正确处理 fetch_markdown 工具调用 (Should handle fetch_markdown tool call correctly)', async () => {
|
|
149
|
+
// 调用被测试的函数
|
|
150
|
+
registerTools(mockServer);
|
|
151
|
+
// 获取注册的处理程序
|
|
152
|
+
const callToolHandler = mockSetRequestHandler.mock.calls[1][1];
|
|
153
|
+
// 创建请求对象
|
|
154
|
+
const request = {
|
|
155
|
+
params: {
|
|
156
|
+
name: 'fetch_markdown',
|
|
157
|
+
arguments: {
|
|
158
|
+
url: 'https://example.com/readme.md'
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
// 调用处理程序
|
|
163
|
+
const result = await callToolHandler(request);
|
|
164
|
+
// 验证 fetchWithAutoDetect 被调用,且参数正确
|
|
165
|
+
expect(fetchWithAutoDetect).toHaveBeenCalledWith({ url: 'https://example.com/readme.md', method: 'fetch_markdown' }, 'markdown');
|
|
166
|
+
// 验证返回结果
|
|
167
|
+
expect(result).toHaveProperty('content');
|
|
168
|
+
expect(result).toHaveProperty('isError', false);
|
|
169
|
+
expect(result.content[0]).toHaveProperty('type', 'text');
|
|
170
|
+
expect(result.content[0]).toHaveProperty('text', 'mock content');
|
|
171
|
+
});
|
|
172
|
+
test('应该正确处理未知工具调用 (Should handle unknown tool call correctly)', async () => {
|
|
173
|
+
// 调用被测试的函数
|
|
174
|
+
registerTools(mockServer);
|
|
175
|
+
// 获取注册的处理程序
|
|
176
|
+
const callToolHandler = mockSetRequestHandler.mock.calls[1][1];
|
|
177
|
+
// 创建请求对象
|
|
178
|
+
const request = {
|
|
179
|
+
params: {
|
|
180
|
+
name: 'unknown_tool',
|
|
181
|
+
arguments: {
|
|
182
|
+
url: 'https://example.com'
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
// 设置模拟返回值
|
|
187
|
+
fetchWithAutoDetect.mockResolvedValue({
|
|
188
|
+
isError: true,
|
|
189
|
+
content: [{ text: 'Unknown tool: unknown_tool' }]
|
|
190
|
+
});
|
|
191
|
+
// 调用处理程序
|
|
192
|
+
const result = await callToolHandler(request);
|
|
193
|
+
// 验证返回结果
|
|
194
|
+
expect(result).toHaveProperty('content');
|
|
195
|
+
expect(result).toHaveProperty('isError', true);
|
|
196
|
+
expect(result.content[0]).toHaveProperty('text');
|
|
197
|
+
expect(result.content[0].text).toContain('Unknown tool');
|
|
198
|
+
});
|
|
199
|
+
test('应该正确处理工具调用错误 (Should handle tool call error correctly)', async () => {
|
|
200
|
+
// 调用被测试的函数
|
|
201
|
+
registerTools(mockServer);
|
|
202
|
+
// 获取注册的处理程序
|
|
203
|
+
const callToolHandler = mockSetRequestHandler.mock.calls[1][1];
|
|
204
|
+
// 创建请求对象
|
|
205
|
+
const request = {
|
|
206
|
+
params: {
|
|
207
|
+
name: 'fetch_html',
|
|
208
|
+
arguments: {
|
|
209
|
+
url: 'https://example.com'
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
// 设置模拟抛出错误
|
|
214
|
+
fetchWithAutoDetect.mockRejectedValue(new Error('Fetch failed'));
|
|
215
|
+
// 调用处理程序
|
|
216
|
+
const result = await callToolHandler(request);
|
|
217
|
+
// 验证返回结果
|
|
218
|
+
expect(result).toHaveProperty('content');
|
|
219
|
+
expect(result).toHaveProperty('isError', true);
|
|
220
|
+
expect(result.content[0]).toHaveProperty('text');
|
|
221
|
+
expect(result.content[0].text).toContain('Fetch failed');
|
|
222
|
+
});
|
|
223
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
// 导入Vitest的全局函数 (Import Vitest global functions)
|
|
7
|
+
import { beforeAll, afterAll, vi } from 'vitest';
|
|
8
|
+
// 全局模拟设置
|
|
9
|
+
// 将 vi 添加到全局,以便在测试中使用
|
|
10
|
+
global.vi = vi;
|
|
11
|
+
// 为了兼容 Jest 的 jest.fn(), jest.mock() 等
|
|
12
|
+
global.jest = {
|
|
13
|
+
fn: vi.fn,
|
|
14
|
+
mock: vi.mock,
|
|
15
|
+
spyOn: vi.spyOn,
|
|
16
|
+
clearAllMocks: vi.clearAllMocks,
|
|
17
|
+
resetAllMocks: vi.resetAllMocks,
|
|
18
|
+
restoreAllMocks: vi.restoreAllMocks
|
|
19
|
+
};
|
|
20
|
+
// 设置测试环境 (Setup test environment)
|
|
21
|
+
process.env.NODE_ENV = 'test';
|
|
22
|
+
// 模拟控制台输出,避免测试时输出过多日志 (Mock console output to avoid too many logs during testing)
|
|
23
|
+
console.log = vi.fn();
|
|
24
|
+
console.info = vi.fn();
|
|
25
|
+
console.warn = vi.fn();
|
|
26
|
+
console.error = vi.fn();
|
|
27
|
+
// 在所有测试开始前的操作 (Operations before all tests)
|
|
28
|
+
beforeAll(() => {
|
|
29
|
+
// 全局设置,在所有测试开始前运行
|
|
30
|
+
// 可以在这里添加全局设置 (Add global setup here)
|
|
31
|
+
});
|
|
32
|
+
// 在所有测试结束后的操作 (Operations after all tests)
|
|
33
|
+
afterAll(() => {
|
|
34
|
+
// 全局清理,在所有测试结束后运行
|
|
35
|
+
// 可以在这里添加全局清理 (Add global cleanup here)
|
|
36
|
+
});
|