mcp-message-test 1.0.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 +79 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +23 -0
- package/dist/schemas.d.ts +9 -0
- package/dist/schemas.js +6 -0
- package/dist/tools/gen_audio.d.ts +2 -0
- package/dist/tools/gen_audio.js +41 -0
- package/dist/tools/gen_embedded_resource.d.ts +2 -0
- package/dist/tools/gen_embedded_resource.js +52 -0
- package/dist/tools/gen_image.d.ts +2 -0
- package/dist/tools/gen_image.js +90 -0
- package/dist/tools/gen_image_uri.d.ts +2 -0
- package/dist/tools/gen_image_uri.js +30 -0
- package/dist/tools/gen_invalid.d.ts +2 -0
- package/dist/tools/gen_invalid.js +124 -0
- package/dist/tools/gen_mixed.d.ts +2 -0
- package/dist/tools/gen_mixed.js +68 -0
- package/dist/tools/gen_resource_link.d.ts +2 -0
- package/dist/tools/gen_resource_link.js +84 -0
- package/dist/tools/gen_text.d.ts +2 -0
- package/dist/tools/gen_text.js +84 -0
- package/dist/utils/annotations.d.ts +5 -0
- package/dist/utils/annotations.js +11 -0
- package/dist/utils/assets.d.ts +5 -0
- package/dist/utils/assets.js +13 -0
- package/dist/utils/audio.d.ts +4 -0
- package/dist/utils/audio.js +439 -0
- package/dist/utils/fetch.d.ts +5 -0
- package/dist/utils/fetch.js +9 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/random.d.ts +8 -0
- package/dist/utils/random.js +20 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# tool-message-test-mcp
|
|
2
|
+
|
|
3
|
+
一个用于测试 MCP Tool 各种消息类型返回的测试服务器。覆盖了文本、图片、音频、资源链接、嵌入式资源、结构化内容、annotations 以及各种边界/异常 case。
|
|
4
|
+
|
|
5
|
+
## 使用
|
|
6
|
+
|
|
7
|
+
### 通过 bunx 直接运行(无需 clone)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bunx github:acdzh/tool-message-test-mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### 本地开发
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# 安装依赖
|
|
17
|
+
bun install
|
|
18
|
+
|
|
19
|
+
# 运行
|
|
20
|
+
bun run index.ts
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 在 MCP 客户端中配置
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"mcpServers": {
|
|
28
|
+
"message-test": {
|
|
29
|
+
"command": "npx",
|
|
30
|
+
"args": ["mcp-message-test"]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"message-test": {
|
|
40
|
+
"command": "bunx",
|
|
41
|
+
"args": ["github:acdzh/tool-message-test-mcp"]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"message-test": {
|
|
51
|
+
"command": "bun run",
|
|
52
|
+
"args": ["/Users/admin/dev/work/mcp/server/message-test/index.ts"]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 提供的 Tools
|
|
59
|
+
|
|
60
|
+
| Tool | 描述 |
|
|
61
|
+
|------|------|
|
|
62
|
+
| `test_text` | 各种文本内容(plain/multi/markdown/unicode/long/empty) |
|
|
63
|
+
| `test_image` | 图片 base64 编码(png/jpg/gif/webp/svg/multi) |
|
|
64
|
+
| `test_image_uri` | image-uri 类型(basic/full/multi_scheme) |
|
|
65
|
+
| `test_audio` | 音频 base64 编码(wav/mp3) |
|
|
66
|
+
| `test_resource_link` | 资源链接(basic/full) |
|
|
67
|
+
| `test_embedded_resource` | 嵌入式资源(text/blob/with_annotations) |
|
|
68
|
+
| `test_mixed_content` | 混合所有标准内容类型 |
|
|
69
|
+
| `test_annotations` | Annotations 测试 |
|
|
70
|
+
| `test_structured_content` | 结构化内容 + outputSchema |
|
|
71
|
+
| `test_error` | 错误响应 |
|
|
72
|
+
| `test_large_content` | 大量 content items |
|
|
73
|
+
| `test_invalid_types` | 非法内容类型 |
|
|
74
|
+
| `test_invalid_structure` | 非法 content 结构 |
|
|
75
|
+
| `test_invalid_fields` | 字段缺失或错误 |
|
|
76
|
+
| `test_invalid_annotations` | 非法 annotations |
|
|
77
|
+
| `test_invalid_result_fields` | 非法结果字段 |
|
|
78
|
+
| `test_throws` | 工具抛出异常 |
|
|
79
|
+
| `test_mixed_valid_invalid` | 混合有效和无效 items |
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer, StdioServerTransport } from '@byted/modelcontextprotocol-server';
|
|
3
|
+
import { registerGenText } from './tools/gen_text.js';
|
|
4
|
+
import { registerGenImage } from './tools/gen_image.js';
|
|
5
|
+
import { registerGenImageUri } from './tools/gen_image_uri.js';
|
|
6
|
+
import { registerGenAudio } from './tools/gen_audio.js';
|
|
7
|
+
import { registerGenResourceLink } from './tools/gen_resource_link.js';
|
|
8
|
+
import { registerGenEmbeddedResource } from './tools/gen_embedded_resource.js';
|
|
9
|
+
import { registerGenMixed } from './tools/gen_mixed.js';
|
|
10
|
+
import { registerGenInvalid } from './tools/gen_invalid.js';
|
|
11
|
+
const server = new McpServer({ name: 'message-test-server', version: '2.0.0' }, { capabilities: { tools: { listChanged: false } } });
|
|
12
|
+
// 注册所有 tools
|
|
13
|
+
registerGenText(server);
|
|
14
|
+
registerGenImage(server);
|
|
15
|
+
registerGenImageUri(server);
|
|
16
|
+
registerGenAudio(server);
|
|
17
|
+
registerGenResourceLink(server);
|
|
18
|
+
registerGenEmbeddedResource(server);
|
|
19
|
+
registerGenMixed(server);
|
|
20
|
+
registerGenInvalid(server);
|
|
21
|
+
// 启动服务器
|
|
22
|
+
const transport = new StdioServerTransport();
|
|
23
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
export declare const annotationsSchema: z.ZodOptional<z.ZodObject<{
|
|
3
|
+
audience: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
4
|
+
user: "user";
|
|
5
|
+
assistant: "assistant";
|
|
6
|
+
}>>>;
|
|
7
|
+
priority: z.ZodOptional<z.ZodNumber>;
|
|
8
|
+
}, z.core.$strip>>;
|
|
9
|
+
export declare const structuredSchema: z.ZodDefault<z.ZodBoolean>;
|
package/dist/schemas.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
export const annotationsSchema = z.object({
|
|
3
|
+
audience: z.array(z.enum(['user', 'assistant'])).optional().describe('内容受众'),
|
|
4
|
+
priority: z.number().min(0).max(1).optional().describe('优先级 0-1'),
|
|
5
|
+
}).optional().describe('MCP annotations,不传则不附加');
|
|
6
|
+
export const structuredSchema = z.boolean().default(false).describe('是否额外返回 structuredContent');
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
import { annotationsSchema, structuredSchema } from '../schemas.js';
|
|
3
|
+
import { buildAnnotations, generateWav, generateMelodyWav } from '../utils/index.js';
|
|
4
|
+
export function registerGenAudio(server) {
|
|
5
|
+
server.registerTool('gen_audio', {
|
|
6
|
+
title: 'Generate Audio',
|
|
7
|
+
description: '本地生成音频,返回 base64 编码。支持单音正弦波或随机旋律',
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
duration_ms: z.number().min(100).max(30000).default(2000).describe('音频时长(毫秒)'),
|
|
10
|
+
mode: z.enum(['tone', 'melody']).default('melody').describe('tone: 单一频率正弦波; melody: 随机旋律'),
|
|
11
|
+
frequency: z.number().min(20).max(20000).default(440).describe('频率 Hz(仅 tone 模式有效)'),
|
|
12
|
+
count: z.number().min(1).max(10).default(1).describe('音频数量'),
|
|
13
|
+
annotations: annotationsSchema,
|
|
14
|
+
structured: structuredSchema,
|
|
15
|
+
}),
|
|
16
|
+
}, async ({ duration_ms, mode, frequency, count, annotations, structured }) => {
|
|
17
|
+
const ann = buildAnnotations(annotations);
|
|
18
|
+
const content = Array.from({ length: count }, (_, i) => {
|
|
19
|
+
let wav;
|
|
20
|
+
if (mode === 'melody') {
|
|
21
|
+
wav = generateMelodyWav(duration_ms);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
// 每个用不同频率增加变化
|
|
25
|
+
const freq = frequency + i * 50;
|
|
26
|
+
wav = generateWav(duration_ms, freq);
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
type: 'audio',
|
|
30
|
+
data: wav.toString('base64'),
|
|
31
|
+
mimeType: 'audio/wav',
|
|
32
|
+
...(ann ? { annotations: ann } : {}),
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
const result = { content };
|
|
36
|
+
if (structured) {
|
|
37
|
+
result.structuredContent = { duration_ms, mode, frequency, count, generatedAt: new Date().toISOString() };
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { annotationsSchema, structuredSchema } from '../schemas.js';
|
|
4
|
+
import { assetsDir, loadAssetBase64, loadAssetText, buildAnnotations, randPick } from '../utils/index.js';
|
|
5
|
+
export function registerGenEmbeddedResource(server) {
|
|
6
|
+
server.registerTool('gen_embedded_resource', {
|
|
7
|
+
title: 'Generate Embedded Resource',
|
|
8
|
+
description: '生成嵌入式资源内容(text 或 blob)',
|
|
9
|
+
inputSchema: z.object({
|
|
10
|
+
resource_type: z.enum(['text', 'blob']).default('text').describe('资源类型'),
|
|
11
|
+
count: z.number().min(1).max(20).default(1).describe('数量'),
|
|
12
|
+
annotations: annotationsSchema,
|
|
13
|
+
structured: structuredSchema,
|
|
14
|
+
}),
|
|
15
|
+
}, async ({ resource_type, count, annotations, structured }) => {
|
|
16
|
+
const ann = buildAnnotations(annotations);
|
|
17
|
+
const content = Array.from({ length: count }, () => {
|
|
18
|
+
if (resource_type === 'text') {
|
|
19
|
+
const files = ['sample.ts', 'sample.txt'];
|
|
20
|
+
const file = randPick(files);
|
|
21
|
+
const mime = file.endsWith('.ts') ? 'text/typescript' : 'text/plain';
|
|
22
|
+
return {
|
|
23
|
+
type: 'resource',
|
|
24
|
+
resource: {
|
|
25
|
+
uri: `file://${resolve(assetsDir, file)}`,
|
|
26
|
+
mimeType: mime,
|
|
27
|
+
text: loadAssetText(file),
|
|
28
|
+
...(ann ? { annotations: ann } : {}),
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
// blob
|
|
33
|
+
const files = ['sample.png', 'sample.jpg', 'sample.webp'];
|
|
34
|
+
const file = randPick(files);
|
|
35
|
+
const mimeMap = { 'sample.png': 'image/png', 'sample.jpg': 'image/jpeg', 'sample.webp': 'image/webp' };
|
|
36
|
+
return {
|
|
37
|
+
type: 'resource',
|
|
38
|
+
resource: {
|
|
39
|
+
uri: `file://${resolve(assetsDir, file)}`,
|
|
40
|
+
mimeType: mimeMap[file],
|
|
41
|
+
blob: loadAssetBase64(file),
|
|
42
|
+
...(ann ? { annotations: ann } : {}),
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
const result = { content };
|
|
47
|
+
if (structured) {
|
|
48
|
+
result.structuredContent = { resource_type, count, generatedAt: new Date().toISOString() };
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
import sharp from 'sharp';
|
|
3
|
+
import { annotationsSchema, structuredSchema } from '../schemas.js';
|
|
4
|
+
import { buildAnnotations } from '../utils/index.js';
|
|
5
|
+
/** 从 picsum 获取一张真实照片 buffer */
|
|
6
|
+
async function fetchPicsumBuffer(width, height, seed) {
|
|
7
|
+
const url = `https://picsum.photos/${width}/${height}.jpg?random=${seed}`;
|
|
8
|
+
const resp = await fetch(url, { redirect: 'follow' });
|
|
9
|
+
if (!resp.ok)
|
|
10
|
+
throw new Error(`Fetch failed: ${resp.status} ${resp.statusText}`);
|
|
11
|
+
return Buffer.from(await resp.arrayBuffer());
|
|
12
|
+
}
|
|
13
|
+
/** 用多张图拼成动态 GIF */
|
|
14
|
+
async function generateAnimatedGif(width, height, frameCount = 4) {
|
|
15
|
+
// 获取多张不同的真实照片作为帧
|
|
16
|
+
const frames = await Promise.all(Array.from({ length: frameCount }, (_, i) => fetchPicsumBuffer(width, height, Date.now() + i * 1000 + Math.random() * 9999)));
|
|
17
|
+
// 将每帧转为统一尺寸的 raw RGBA 数据
|
|
18
|
+
const rawFrames = [];
|
|
19
|
+
for (const frame of frames) {
|
|
20
|
+
const raw = await sharp(frame).resize(width, height).ensureAlpha().raw().toBuffer();
|
|
21
|
+
rawFrames.push(raw);
|
|
22
|
+
}
|
|
23
|
+
// 将所有帧垂直拼接成一个 tall image (width x height*frameCount)
|
|
24
|
+
const combined = Buffer.concat(rawFrames);
|
|
25
|
+
// sharp 通过 raw input + pages 参数识别为多帧动图
|
|
26
|
+
return sharp(combined, {
|
|
27
|
+
raw: {
|
|
28
|
+
width,
|
|
29
|
+
height: height * frameCount,
|
|
30
|
+
channels: 4,
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
.gif({
|
|
34
|
+
delay: Array(frameCount).fill(500),
|
|
35
|
+
loop: 0,
|
|
36
|
+
})
|
|
37
|
+
.toBuffer();
|
|
38
|
+
}
|
|
39
|
+
export function registerGenImage(server) {
|
|
40
|
+
server.registerTool('gen_image', {
|
|
41
|
+
title: 'Generate Image',
|
|
42
|
+
description: '获取随机真实图片,返回 base64 编码。gif 为多帧动图',
|
|
43
|
+
inputSchema: z.object({
|
|
44
|
+
width: z.number().min(1).max(4096).default(400).describe('图片宽度'),
|
|
45
|
+
height: z.number().min(1).max(4096).default(300).describe('图片高度'),
|
|
46
|
+
format: z.enum(['jpg', 'png', 'webp', 'gif', 'avif']).default('jpg').describe('图片格式'),
|
|
47
|
+
count: z.number().min(1).max(20).default(1).describe('图片数量'),
|
|
48
|
+
gif_frames: z.number().min(2).max(10).default(4).describe('GIF 动图帧数(仅 gif 格式有效)'),
|
|
49
|
+
annotations: annotationsSchema,
|
|
50
|
+
structured: structuredSchema,
|
|
51
|
+
}),
|
|
52
|
+
}, async ({ width, height, format, count, gif_frames, annotations, structured }) => {
|
|
53
|
+
const mimeMap = {
|
|
54
|
+
jpg: 'image/jpeg',
|
|
55
|
+
png: 'image/png',
|
|
56
|
+
webp: 'image/webp',
|
|
57
|
+
gif: 'image/gif',
|
|
58
|
+
avif: 'image/avif',
|
|
59
|
+
};
|
|
60
|
+
const ann = buildAnnotations(annotations);
|
|
61
|
+
const content = await Promise.all(Array.from({ length: count }, async (_, i) => {
|
|
62
|
+
let outputBuffer;
|
|
63
|
+
if (format === 'gif') {
|
|
64
|
+
// GIF: 获取多张照片拼成动图
|
|
65
|
+
outputBuffer = await generateAnimatedGif(width, height, gif_frames);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// 其他格式: 获取单张照片,按需转换
|
|
69
|
+
const srcBuffer = await fetchPicsumBuffer(width, height, Date.now() + i);
|
|
70
|
+
if (format === 'jpg') {
|
|
71
|
+
outputBuffer = srcBuffer;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
outputBuffer = await sharp(srcBuffer).toFormat(format).toBuffer();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
type: 'image',
|
|
79
|
+
data: outputBuffer.toString('base64'),
|
|
80
|
+
mimeType: mimeMap[format],
|
|
81
|
+
...(ann ? { annotations: ann } : {}),
|
|
82
|
+
};
|
|
83
|
+
}));
|
|
84
|
+
const result = { content };
|
|
85
|
+
if (structured) {
|
|
86
|
+
result.structuredContent = { width, height, format, count, generatedAt: new Date().toISOString() };
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
import { annotationsSchema, structuredSchema } from '../schemas.js';
|
|
3
|
+
import { buildAnnotations, randInt } from '../utils/index.js';
|
|
4
|
+
export function registerGenImageUri(server) {
|
|
5
|
+
server.registerTool('gen_image_uri', {
|
|
6
|
+
title: 'Generate Image URI',
|
|
7
|
+
description: '返回 image-uri 类型的图片引用(真实可访问的 URL)',
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
width: z.number().min(1).max(4096).default(400).describe('图片宽度'),
|
|
10
|
+
height: z.number().min(1).max(4096).default(300).describe('图片高度'),
|
|
11
|
+
count: z.number().min(1).max(20).default(1).describe('数量'),
|
|
12
|
+
annotations: annotationsSchema,
|
|
13
|
+
structured: structuredSchema,
|
|
14
|
+
}),
|
|
15
|
+
}, async ({ width, height, count, annotations, structured }) => {
|
|
16
|
+
const ann = buildAnnotations(annotations);
|
|
17
|
+
const content = Array.from({ length: count }, (_, i) => ({
|
|
18
|
+
type: 'image-uri',
|
|
19
|
+
// picsum 的 URL 会 302 到真实图片,每次带不同随机种子保证不同
|
|
20
|
+
uri: `https://picsum.photos/${width}/${height}?random=${Date.now()}_${i}_${randInt(1000, 9999)}`,
|
|
21
|
+
mimeType: 'image/jpeg',
|
|
22
|
+
...(ann ? { annotations: ann } : {}),
|
|
23
|
+
}));
|
|
24
|
+
const result = { content };
|
|
25
|
+
if (structured) {
|
|
26
|
+
result.structuredContent = { width, height, count, generatedAt: new Date().toISOString() };
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { assetsDir, loadAssetBase64, randSentence, randInt } from '../utils/index.js';
|
|
4
|
+
export function registerGenInvalid(server) {
|
|
5
|
+
server.registerTool('gen_invalid', {
|
|
6
|
+
title: 'Generate Invalid Response',
|
|
7
|
+
description: '生成各种不符合规范的响应,用于测试客户端的健壮性',
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
kind: z.enum([
|
|
10
|
+
// 错误响应
|
|
11
|
+
'is_error',
|
|
12
|
+
'is_error_detailed',
|
|
13
|
+
'is_error_wrong_type',
|
|
14
|
+
// 抛出异常
|
|
15
|
+
'throws',
|
|
16
|
+
// 非法类型
|
|
17
|
+
'unknown_type',
|
|
18
|
+
'empty_type',
|
|
19
|
+
'missing_type',
|
|
20
|
+
'wrong_field_type',
|
|
21
|
+
// 非法结构
|
|
22
|
+
'empty_content',
|
|
23
|
+
'null_content',
|
|
24
|
+
'not_array',
|
|
25
|
+
'no_content_field',
|
|
26
|
+
'null_item',
|
|
27
|
+
'returns_undefined',
|
|
28
|
+
// 非法字段
|
|
29
|
+
'image_no_data',
|
|
30
|
+
'image_no_mime',
|
|
31
|
+
'bad_base64',
|
|
32
|
+
'audio_no_mime',
|
|
33
|
+
'image_uri_no_uri',
|
|
34
|
+
'resource_link_no_uri',
|
|
35
|
+
'resource_no_resource',
|
|
36
|
+
// 非法 annotations
|
|
37
|
+
'annotations_wrong_type',
|
|
38
|
+
'annotations_bad_priority',
|
|
39
|
+
'annotations_bad_audience',
|
|
40
|
+
'extra_fields',
|
|
41
|
+
// 混合有效和无效
|
|
42
|
+
'mixed_valid_invalid',
|
|
43
|
+
]).describe('异常类型'),
|
|
44
|
+
}),
|
|
45
|
+
}, async ({ kind }) => {
|
|
46
|
+
switch (kind) {
|
|
47
|
+
// --- 错误响应 ---
|
|
48
|
+
case 'is_error':
|
|
49
|
+
return { content: [{ type: 'text', text: `Error: ${randSentence()}` }], isError: true };
|
|
50
|
+
case 'is_error_detailed':
|
|
51
|
+
return {
|
|
52
|
+
content: [
|
|
53
|
+
{ type: 'text', text: 'Error: API rate limit exceeded.' },
|
|
54
|
+
{ type: 'text', text: `Retry after: ${randInt(10, 120)} seconds.` },
|
|
55
|
+
{ type: 'text', text: `Request ID: req-${crypto.randomUUID().slice(0, 8)}` },
|
|
56
|
+
],
|
|
57
|
+
isError: true,
|
|
58
|
+
};
|
|
59
|
+
case 'is_error_wrong_type':
|
|
60
|
+
return { content: [{ type: 'text', text: 'Error flag is wrong type.' }], isError: 'yes' };
|
|
61
|
+
// --- 抛出异常 ---
|
|
62
|
+
case 'throws':
|
|
63
|
+
throw new Error(`Unexpected server error: ${randSentence()}`);
|
|
64
|
+
// --- 非法类型 ---
|
|
65
|
+
case 'unknown_type':
|
|
66
|
+
return { content: [{ type: 'video', data: 'some-data', mimeType: 'video/mp4' }] };
|
|
67
|
+
case 'empty_type':
|
|
68
|
+
return { content: [{ type: '', text: randSentence() }] };
|
|
69
|
+
case 'missing_type':
|
|
70
|
+
return { content: [{ text: randSentence() }] };
|
|
71
|
+
case 'wrong_field_type':
|
|
72
|
+
return { content: [{ type: 'text', text: randInt(1, 99999) }] };
|
|
73
|
+
// --- 非法结构 ---
|
|
74
|
+
case 'empty_content':
|
|
75
|
+
return { content: [] };
|
|
76
|
+
case 'null_content':
|
|
77
|
+
return { content: null };
|
|
78
|
+
case 'not_array':
|
|
79
|
+
return { content: { type: 'text', text: randSentence() } };
|
|
80
|
+
case 'no_content_field':
|
|
81
|
+
return { result: randSentence() };
|
|
82
|
+
case 'null_item':
|
|
83
|
+
return { content: [{ type: 'text', text: 'Valid.' }, null, { type: 'text', text: 'Also valid.' }] };
|
|
84
|
+
case 'returns_undefined':
|
|
85
|
+
return undefined;
|
|
86
|
+
// --- 非法字段 ---
|
|
87
|
+
case 'image_no_data':
|
|
88
|
+
return { content: [{ type: 'image', mimeType: 'image/png' }] };
|
|
89
|
+
case 'image_no_mime':
|
|
90
|
+
return { content: [{ type: 'image', data: loadAssetBase64('sample.png') }] };
|
|
91
|
+
case 'bad_base64':
|
|
92
|
+
return { content: [{ type: 'image', data: '!!!not-valid-base64@@@###', mimeType: 'image/png' }] };
|
|
93
|
+
case 'audio_no_mime':
|
|
94
|
+
return { content: [{ type: 'audio', data: loadAssetBase64('sample_3s.mp3') }] };
|
|
95
|
+
case 'image_uri_no_uri':
|
|
96
|
+
return { content: [{ type: 'image-uri', mimeType: 'image/png' }] };
|
|
97
|
+
case 'resource_link_no_uri':
|
|
98
|
+
return { content: [{ type: 'resource_link', description: 'A link with no URI or name' }] };
|
|
99
|
+
case 'resource_no_resource':
|
|
100
|
+
return { content: [{ type: 'resource' }] };
|
|
101
|
+
// --- 非法 annotations ---
|
|
102
|
+
case 'annotations_wrong_type':
|
|
103
|
+
return { content: [{ type: 'text', text: randSentence(), annotations: 'should-be-an-object' }] };
|
|
104
|
+
case 'annotations_bad_priority':
|
|
105
|
+
return { content: [{ type: 'text', text: randSentence(), annotations: { audience: ['user'], priority: 99.9 } }] };
|
|
106
|
+
case 'annotations_bad_audience':
|
|
107
|
+
return { content: [{ type: 'text', text: randSentence(), annotations: { audience: ['user', 'robot', 'admin'], priority: 0.5 } }] };
|
|
108
|
+
case 'extra_fields':
|
|
109
|
+
return { content: [{ type: 'text', text: randSentence(), unknownField1: 'value1', unknownField2: 42, nested: { deep: true } }] };
|
|
110
|
+
// --- 混合有效和无效 ---
|
|
111
|
+
case 'mixed_valid_invalid':
|
|
112
|
+
return {
|
|
113
|
+
content: [
|
|
114
|
+
{ type: 'text', text: 'Valid text item.' },
|
|
115
|
+
{ type: 'unknown_type', data: 'mystery' },
|
|
116
|
+
{ type: 'image', data: '!!!not-base64', mimeType: 'image/png' },
|
|
117
|
+
{ type: 'text', text: 'Another valid text.' },
|
|
118
|
+
null,
|
|
119
|
+
{ type: 'resource_link', uri: `file://${resolve(assetsDir, 'sample.txt')}`, name: 'sample.txt' },
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { annotationsSchema, structuredSchema } from '../schemas.js';
|
|
4
|
+
import { assetsDir, fetchBase64, loadAssetText, buildAnnotations, randInt, randPick, randParagraph, generateWav } from '../utils/index.js';
|
|
5
|
+
export function registerGenMixed(server) {
|
|
6
|
+
server.registerTool('gen_mixed', {
|
|
7
|
+
title: 'Generate Mixed Content',
|
|
8
|
+
description: '在一次响应中返回多种内容类型的混合',
|
|
9
|
+
inputSchema: z.object({
|
|
10
|
+
types: z.array(z.enum(['text', 'image', 'image_uri', 'audio', 'resource_link', 'resource']))
|
|
11
|
+
.default(['text', 'image', 'resource_link'])
|
|
12
|
+
.describe('要包含的内容类型'),
|
|
13
|
+
count_per_type: z.number().min(1).max(5).default(1).describe('每种类型的数量'),
|
|
14
|
+
annotations: annotationsSchema,
|
|
15
|
+
structured: structuredSchema,
|
|
16
|
+
}),
|
|
17
|
+
}, async ({ types, count_per_type, annotations, structured }) => {
|
|
18
|
+
const ann = buildAnnotations(annotations);
|
|
19
|
+
const content = [];
|
|
20
|
+
for (const type of types) {
|
|
21
|
+
for (let i = 0; i < count_per_type; i++) {
|
|
22
|
+
switch (type) {
|
|
23
|
+
case 'text':
|
|
24
|
+
content.push({ type: 'text', text: randParagraph(3), ...(ann ? { annotations: ann } : {}) });
|
|
25
|
+
break;
|
|
26
|
+
case 'image': {
|
|
27
|
+
const w = randInt(200, 600);
|
|
28
|
+
const h = randInt(200, 400);
|
|
29
|
+
const { data, mimeType } = await fetchBase64(`https://picsum.photos/${w}/${h}?random=${Date.now()}_${i}`);
|
|
30
|
+
content.push({ type: 'image', data, mimeType, ...(ann ? { annotations: ann } : {}) });
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
case 'image_uri':
|
|
34
|
+
content.push({
|
|
35
|
+
type: 'image-uri', uri: `https://picsum.photos/${randInt(200, 600)}/${randInt(200, 400)}?random=${Date.now()}_${i}`,
|
|
36
|
+
mimeType: 'image/jpeg', ...(ann ? { annotations: ann } : {}),
|
|
37
|
+
});
|
|
38
|
+
break;
|
|
39
|
+
case 'audio': {
|
|
40
|
+
const wav = generateWav(randInt(500, 2000), randInt(200, 800));
|
|
41
|
+
content.push({ type: 'audio', data: wav.toString('base64'), mimeType: 'audio/wav', ...(ann ? { annotations: ann } : {}) });
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
case 'resource_link': {
|
|
45
|
+
const file = randPick(['sample.ts', 'sample.txt', 'sample.png']);
|
|
46
|
+
content.push({
|
|
47
|
+
type: 'resource_link', uri: `file://${resolve(assetsDir, file)}`,
|
|
48
|
+
name: file, mimeType: 'application/octet-stream', ...(ann ? { annotations: ann } : {}),
|
|
49
|
+
});
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case 'resource':
|
|
53
|
+
content.push({
|
|
54
|
+
type: 'resource',
|
|
55
|
+
resource: { uri: `file://${resolve(assetsDir, 'sample.txt')}`, mimeType: 'text/plain', text: loadAssetText('sample.txt') },
|
|
56
|
+
...(ann ? { annotations: ann } : {}),
|
|
57
|
+
});
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const result = { content };
|
|
63
|
+
if (structured) {
|
|
64
|
+
result.structuredContent = { types, count_per_type, generatedAt: new Date().toISOString() };
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { annotationsSchema, structuredSchema } from '../schemas.js';
|
|
6
|
+
import { buildAnnotations, randPick, randInt, randParagraph, randSentence, generateWav } from '../utils/index.js';
|
|
7
|
+
/** 在临时目录下生成随机文件,返回文件路径和元信息 */
|
|
8
|
+
function generateTempFile() {
|
|
9
|
+
const tempBase = resolve(tmpdir(), 'mcp-message-test');
|
|
10
|
+
mkdirSync(tempBase, { recursive: true });
|
|
11
|
+
const id = crypto.randomUUID().slice(0, 8);
|
|
12
|
+
const generators = [
|
|
13
|
+
() => {
|
|
14
|
+
const name = `text_${id}.txt`;
|
|
15
|
+
const content = randParagraph(randInt(3, 10));
|
|
16
|
+
const filePath = resolve(tempBase, name);
|
|
17
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
18
|
+
return { path: filePath, name, mime: 'text/plain', desc: 'Generated plain text file' };
|
|
19
|
+
},
|
|
20
|
+
() => {
|
|
21
|
+
const name = `code_${id}.ts`;
|
|
22
|
+
const content = `interface ${randPick(['User', 'Config', 'State'])} {\n id: string;\n value: ${randInt(1, 999)};\n}\n\nexport function process(): void {\n console.log("${randSentence()}");\n}\n`;
|
|
23
|
+
const filePath = resolve(tempBase, name);
|
|
24
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
25
|
+
return { path: filePath, name, mime: 'text/typescript', desc: 'Generated TypeScript file' };
|
|
26
|
+
},
|
|
27
|
+
() => {
|
|
28
|
+
const name = `data_${id}.json`;
|
|
29
|
+
const content = JSON.stringify({
|
|
30
|
+
id: crypto.randomUUID(),
|
|
31
|
+
timestamp: new Date().toISOString(),
|
|
32
|
+
values: Array.from({ length: randInt(3, 8) }, () => randInt(1, 1000)),
|
|
33
|
+
message: randSentence(),
|
|
34
|
+
}, null, 2);
|
|
35
|
+
const filePath = resolve(tempBase, name);
|
|
36
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
37
|
+
return { path: filePath, name, mime: 'application/json', desc: 'Generated JSON data file' };
|
|
38
|
+
},
|
|
39
|
+
() => {
|
|
40
|
+
const name = `audio_${id}.wav`;
|
|
41
|
+
const wav = generateWav(randInt(500, 2000), randInt(200, 800));
|
|
42
|
+
const filePath = resolve(tempBase, name);
|
|
43
|
+
writeFileSync(filePath, wav);
|
|
44
|
+
return { path: filePath, name, mime: 'audio/wav', desc: 'Generated WAV audio file' };
|
|
45
|
+
},
|
|
46
|
+
() => {
|
|
47
|
+
const name = `config_${id}.yaml`;
|
|
48
|
+
const content = `name: ${randPick(['app', 'service', 'worker'])}-${id}\nversion: ${randInt(1, 5)}.${randInt(0, 9)}.${randInt(0, 20)}\nport: ${randInt(3000, 9000)}\ndebug: ${randPick(['true', 'false'])}\nlogLevel: ${randPick(['info', 'warn', 'error', 'debug'])}\n`;
|
|
49
|
+
const filePath = resolve(tempBase, name);
|
|
50
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
51
|
+
return { path: filePath, name, mime: 'text/yaml', desc: 'Generated YAML config file' };
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
return randPick(generators)();
|
|
55
|
+
}
|
|
56
|
+
export function registerGenResourceLink(server) {
|
|
57
|
+
server.registerTool('gen_resource_link', {
|
|
58
|
+
title: 'Generate Resource Link',
|
|
59
|
+
description: '动态生成文件到临时目录,返回 resource_link',
|
|
60
|
+
inputSchema: z.object({
|
|
61
|
+
count: z.number().min(1).max(20).default(1).describe('数量'),
|
|
62
|
+
annotations: annotationsSchema,
|
|
63
|
+
structured: structuredSchema,
|
|
64
|
+
}),
|
|
65
|
+
}, async ({ count, annotations, structured }) => {
|
|
66
|
+
const ann = buildAnnotations(annotations);
|
|
67
|
+
const content = Array.from({ length: count }, () => {
|
|
68
|
+
const file = generateTempFile();
|
|
69
|
+
return {
|
|
70
|
+
type: 'resource_link',
|
|
71
|
+
uri: `file://${file.path}`,
|
|
72
|
+
name: file.name,
|
|
73
|
+
description: file.desc,
|
|
74
|
+
mimeType: file.mime,
|
|
75
|
+
...(ann ? { annotations: ann } : {}),
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
const result = { content };
|
|
79
|
+
if (structured) {
|
|
80
|
+
result.structuredContent = { count, generatedAt: new Date().toISOString() };
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
});
|
|
84
|
+
}
|