@zzp123/mcp-zentao 1.18.2 → 1.18.4
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/CHANGELOG.md +48 -0
- package/README.md +39 -0
- package/dist/api/zentaoApi.js +1 -266
- package/dist/config.js +1 -1
- package/dist/index-dev.js +4 -23
- package/dist/index-pm.js +70 -85
- package/dist/index-qa.js +1 -23
- package/dist/index.js +74 -130
- package/dist/mcpHelpers.d.ts +13 -0
- package/dist/mcpHelpers.js +24 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { ZentaoAPI } from './api/zentaoApi.js';
|
|
6
6
|
import { loadConfig, saveConfig } from './config.js';
|
|
7
|
+
import { withApi } from './mcpHelpers.js';
|
|
7
8
|
// 解析命令行参数
|
|
8
9
|
const args = process.argv.slice(2);
|
|
9
10
|
let configData = null;
|
|
@@ -14,15 +15,12 @@ if (configIndex !== -1 && configIndex + 1 < args.length) {
|
|
|
14
15
|
// 获取 --config 后面的 JSON 字符串并解析
|
|
15
16
|
const jsonStr = args[configIndex + 1];
|
|
16
17
|
configData = JSON.parse(jsonStr);
|
|
17
|
-
console.log('成功解析配置数据:', configData);
|
|
18
18
|
// 如果配置数据中包含 config 对象,则保存配置
|
|
19
19
|
if (configData.config) {
|
|
20
|
-
console.log('正在保存配置...');
|
|
21
20
|
saveConfig(configData.config);
|
|
22
21
|
}
|
|
23
22
|
}
|
|
24
23
|
catch (error) {
|
|
25
|
-
console.error('配置解析失败:', error);
|
|
26
24
|
process.exit(1);
|
|
27
25
|
}
|
|
28
26
|
}
|
|
@@ -33,11 +31,10 @@ const server = new McpServer({
|
|
|
33
31
|
});
|
|
34
32
|
// Initialize ZentaoAPI instance
|
|
35
33
|
let zentaoApi = null;
|
|
34
|
+
const getApi = () => zentaoApi;
|
|
36
35
|
export default async function main(params) {
|
|
37
|
-
console.log('接收到的参数:', params);
|
|
38
36
|
// 如果传入了配置信息,就保存它
|
|
39
37
|
if (params.config) {
|
|
40
|
-
console.log('保存新的配置信息...');
|
|
41
38
|
saveConfig(params.config);
|
|
42
39
|
}
|
|
43
40
|
}
|
|
@@ -58,25 +55,21 @@ server.tool("initZentao", {}, async ({}) => {
|
|
|
58
55
|
// Add getMyTasks tool
|
|
59
56
|
server.tool("getMyTasks", {
|
|
60
57
|
status: z.enum(['wait', 'doing', 'done', 'all']).optional()
|
|
61
|
-
}, async ({ status }) => {
|
|
62
|
-
|
|
63
|
-
throw new Error("Please initialize Zentao API first");
|
|
64
|
-
const tasks = await zentaoApi.getMyTasks(status);
|
|
58
|
+
}, withApi(getApi, async (api, { status }) => {
|
|
59
|
+
const tasks = await api.getMyTasks(status);
|
|
65
60
|
return {
|
|
66
61
|
content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }]
|
|
67
62
|
};
|
|
68
|
-
});
|
|
63
|
+
}));
|
|
69
64
|
// Add getTaskDetail tool
|
|
70
65
|
server.tool("getTaskDetail", {
|
|
71
66
|
taskId: z.number()
|
|
72
|
-
}, async ({ taskId }) => {
|
|
73
|
-
|
|
74
|
-
throw new Error("Please initialize Zentao API first");
|
|
75
|
-
const task = await zentaoApi.getTaskDetail(taskId);
|
|
67
|
+
}, withApi(getApi, async (api, { taskId }) => {
|
|
68
|
+
const task = await api.getTaskDetail(taskId);
|
|
76
69
|
return {
|
|
77
70
|
content: [{ type: "text", text: JSON.stringify(task, null, 2) }]
|
|
78
71
|
};
|
|
79
|
-
});
|
|
72
|
+
}));
|
|
80
73
|
// Add getProducts tool
|
|
81
74
|
server.tool("getProducts", "获取产品列表 - 只返回核心字段(id, 名称, 负责人)", {
|
|
82
75
|
fields: z.enum(['basic', 'default', 'full']).optional().describe("返回字段级别:'basic'(id/名称/状态)、'default'(+负责人/创建人,默认)、'full'(完整字段)")
|
|
@@ -564,76 +557,58 @@ server.tool("getProjects", {
|
|
|
564
557
|
};
|
|
565
558
|
});
|
|
566
559
|
// Add changeStory tool
|
|
567
|
-
server.tool("changeStory", "需求变更 - 支持软件需求(story)和用户需求(requirement)
|
|
560
|
+
server.tool("changeStory", "需求变更 - 支持软件需求(story)和用户需求(requirement),自动识别类型", {
|
|
568
561
|
storyId: z.number().describe("需求ID(可以是story或requirement类型)"),
|
|
569
|
-
update: z.
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
}),
|
|
612
|
-
z.string()
|
|
613
|
-
]).describe(`更新需求(使用 PUT 接口)
|
|
562
|
+
update: z.object({
|
|
563
|
+
// 基本信息
|
|
564
|
+
title: z.string().optional(),
|
|
565
|
+
product: z.number().optional(),
|
|
566
|
+
parent: z.number().optional(),
|
|
567
|
+
module: z.number().optional(),
|
|
568
|
+
branch: z.number().optional(),
|
|
569
|
+
plan: z.union([z.number(), z.array(z.number())]).optional(),
|
|
570
|
+
type: z.string().optional(),
|
|
571
|
+
// 来源信息
|
|
572
|
+
source: z.string().optional(),
|
|
573
|
+
sourceNote: z.string().optional(),
|
|
574
|
+
// 分类与优先级
|
|
575
|
+
category: z.string().optional(),
|
|
576
|
+
pri: z.number().optional(),
|
|
577
|
+
estimate: z.number().optional(),
|
|
578
|
+
// 状态与阶段
|
|
579
|
+
stage: z.string().optional(),
|
|
580
|
+
status: z.string().optional(),
|
|
581
|
+
// 关键词与标识
|
|
582
|
+
keywords: z.string().optional(),
|
|
583
|
+
color: z.string().optional(),
|
|
584
|
+
grade: z.number().optional(),
|
|
585
|
+
// 人员相关
|
|
586
|
+
mailto: z.array(z.string()).optional(),
|
|
587
|
+
reviewer: z.array(z.string()).optional().describe("评审人员列表(通常为必填,除非设置needNotReview=true跳过评审)"),
|
|
588
|
+
assignedTo: z.string().optional(),
|
|
589
|
+
closedBy: z.string().optional(),
|
|
590
|
+
feedbackBy: z.string().optional(),
|
|
591
|
+
// 关闭相关
|
|
592
|
+
closedReason: z.enum(['done', 'subdivided', 'duplicate', 'postponed', 'willnotdo', 'cancel', 'bydesign']).optional(),
|
|
593
|
+
duplicateStory: z.number().optional(),
|
|
594
|
+
// 评审相关
|
|
595
|
+
needNotReview: z.boolean().optional().describe("是否跳过评审,如果不提供reviewer则应设置为true"),
|
|
596
|
+
// 通知相关
|
|
597
|
+
notifyEmail: z.string().optional(),
|
|
598
|
+
// 描述内容
|
|
599
|
+
spec: z.string().optional(),
|
|
600
|
+
verify: z.string().optional(),
|
|
601
|
+
// 备注
|
|
602
|
+
comment: z.string().optional()
|
|
603
|
+
}).describe(`更新需求(使用 PUT 接口)
|
|
614
604
|
|
|
615
605
|
此工具使用标准的"修改需求其他字段"接口(PUT /stories/:id),支持修改29个字段。
|
|
616
606
|
自动处理评审人问题,无需手动设置 needNotReview。
|
|
617
|
-
|
|
618
|
-
支持对象或JSON字符串格式。
|
|
619
607
|
`.trim())
|
|
620
608
|
}, async ({ storyId, update }) => {
|
|
621
609
|
if (!zentaoApi)
|
|
622
610
|
throw new Error("Please initialize Zentao API first");
|
|
623
|
-
|
|
624
|
-
let updateData;
|
|
625
|
-
if (typeof update === 'string') {
|
|
626
|
-
try {
|
|
627
|
-
updateData = JSON.parse(update);
|
|
628
|
-
}
|
|
629
|
-
catch (error) {
|
|
630
|
-
throw new Error(`Invalid JSON string: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
else {
|
|
634
|
-
updateData = update;
|
|
635
|
-
}
|
|
636
|
-
const story = await zentaoApi.changeStory(storyId, updateData);
|
|
611
|
+
const story = await zentaoApi.changeStory(storyId, update);
|
|
637
612
|
return {
|
|
638
613
|
content: [{ type: "text", text: JSON.stringify(story, null, 2) }]
|
|
639
614
|
};
|
|
@@ -878,49 +853,32 @@ server.tool("getRequirementDetail", "获取用户需求详情 - 用户需求的
|
|
|
878
853
|
});
|
|
879
854
|
server.tool("changeRequirement", "用户需求变更 - 用户需求的专用接口,提供更好的语义化", {
|
|
880
855
|
requirementId: z.number().describe("用户需求ID"),
|
|
881
|
-
update: z.
|
|
882
|
-
z.
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
}),
|
|
899
|
-
z.string().describe("JSON字符串格式的更新内容")
|
|
900
|
-
]).describe(`
|
|
856
|
+
update: z.object({
|
|
857
|
+
title: z.string().optional(),
|
|
858
|
+
product: z.number().optional(),
|
|
859
|
+
parent: z.number().optional(),
|
|
860
|
+
module: z.number().optional(),
|
|
861
|
+
pri: z.number().optional(),
|
|
862
|
+
category: z.string().optional(),
|
|
863
|
+
spec: z.string().optional(),
|
|
864
|
+
verify: z.string().optional(),
|
|
865
|
+
source: z.string().optional(),
|
|
866
|
+
sourceNote: z.string().optional(),
|
|
867
|
+
estimate: z.number().optional(),
|
|
868
|
+
keywords: z.string().optional(),
|
|
869
|
+
assignedTo: z.string().optional(),
|
|
870
|
+
reviewer: z.array(z.string()).optional(),
|
|
871
|
+
comment: z.string().optional()
|
|
872
|
+
}).describe(`
|
|
901
873
|
更新用户需求(使用 PUT /stories/:id 接口)
|
|
902
874
|
|
|
903
875
|
此工具使用标准的"修改需求其他字段"接口(PUT /stories/:id),支持修改多个字段。
|
|
904
876
|
自动处理评审人问题,无需手动设置 needNotReview。
|
|
905
|
-
|
|
906
|
-
支持对象或JSON字符串格式。
|
|
907
877
|
`.trim())
|
|
908
878
|
}, async ({ requirementId, update }) => {
|
|
909
879
|
if (!zentaoApi)
|
|
910
880
|
throw new Error("Please initialize Zentao API first");
|
|
911
|
-
|
|
912
|
-
if (typeof update === 'string') {
|
|
913
|
-
try {
|
|
914
|
-
updateData = JSON.parse(update);
|
|
915
|
-
}
|
|
916
|
-
catch (e) {
|
|
917
|
-
throw new Error('Invalid JSON string for update parameter');
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
else {
|
|
921
|
-
updateData = update;
|
|
922
|
-
}
|
|
923
|
-
const requirement = await zentaoApi.changeStory(requirementId, updateData);
|
|
881
|
+
const requirement = await zentaoApi.changeStory(requirementId, update);
|
|
924
882
|
return {
|
|
925
883
|
content: [{ type: "text", text: JSON.stringify(requirement, null, 2) }]
|
|
926
884
|
};
|
|
@@ -1311,13 +1269,11 @@ server.tool("uploadImageFromClipboard", "上传剪贴板图片到禅道 - 自动
|
|
|
1311
1269
|
const path = await import('path');
|
|
1312
1270
|
const { execSync } = await import('child_process');
|
|
1313
1271
|
try {
|
|
1314
|
-
console.log('[uploadImageFromClipboard] 开始执行...');
|
|
1315
1272
|
// 读取系统剪贴板图片
|
|
1316
1273
|
let fileBuffer;
|
|
1317
1274
|
let finalFilename;
|
|
1318
1275
|
// Windows: 使用 PowerShell 脚本读取剪贴板
|
|
1319
1276
|
if (process.platform === 'win32') {
|
|
1320
|
-
console.log('[uploadImageFromClipboard] Windows平台,使用PowerShell脚本读取剪贴板...');
|
|
1321
1277
|
// 内嵌 PowerShell 脚本,避免依赖外部文件
|
|
1322
1278
|
const psScript = `Add-Type -AssemblyName System.Windows.Forms; Add-Type -AssemblyName System.Drawing; $img = [System.Windows.Forms.Clipboard]::GetImage(); if ($img) { $ms = New-Object System.IO.MemoryStream; $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); $bytes = $ms.ToArray(); $ms.Close(); $base64 = [Convert]::ToBase64String($bytes); Write-Output $base64 } else { Write-Output 'NoImage' }`;
|
|
1323
1279
|
try {
|
|
@@ -1329,7 +1285,6 @@ server.tool("uploadImageFromClipboard", "上传剪贴板图片到禅道 - 自动
|
|
|
1329
1285
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
1330
1286
|
}).trim();
|
|
1331
1287
|
if (!result || result.includes('NoImage') || result.includes('Error')) {
|
|
1332
|
-
console.error('[uploadImageFromClipboard] 剪贴板中没有图片');
|
|
1333
1288
|
return {
|
|
1334
1289
|
content: [{
|
|
1335
1290
|
type: "text",
|
|
@@ -1343,10 +1298,8 @@ server.tool("uploadImageFromClipboard", "上传剪贴板图片到禅道 - 自动
|
|
|
1343
1298
|
}
|
|
1344
1299
|
fileBuffer = Buffer.from(result, 'base64');
|
|
1345
1300
|
finalFilename = filename || `clipboard_${Date.now()}.png`;
|
|
1346
|
-
console.log(`[uploadImageFromClipboard] 已读取剪贴板图片,大小: ${fileBuffer.length} bytes`);
|
|
1347
1301
|
}
|
|
1348
1302
|
catch (err) {
|
|
1349
|
-
console.error('[uploadImageFromClipboard] Windows读取剪贴板失败:', err);
|
|
1350
1303
|
return {
|
|
1351
1304
|
content: [{
|
|
1352
1305
|
type: "text",
|
|
@@ -1362,17 +1315,14 @@ server.tool("uploadImageFromClipboard", "上传剪贴板图片到禅道 - 自动
|
|
|
1362
1315
|
}
|
|
1363
1316
|
// macOS: 使用 pngpaste
|
|
1364
1317
|
else if (process.platform === 'darwin') {
|
|
1365
|
-
console.log('[uploadImageFromClipboard] macOS平台,使用pngpaste读取剪贴板...');
|
|
1366
1318
|
const tempFile = path.join(process.cwd(), '.temp_clipboard.png');
|
|
1367
1319
|
try {
|
|
1368
1320
|
execSync(`pngpaste "${tempFile}"`);
|
|
1369
1321
|
fileBuffer = fs.readFileSync(tempFile);
|
|
1370
1322
|
fs.unlinkSync(tempFile);
|
|
1371
1323
|
finalFilename = filename || `clipboard_${Date.now()}.png`;
|
|
1372
|
-
console.log(`[uploadImageFromClipboard] 已读取剪贴板图片,大小: ${fileBuffer.length} bytes`);
|
|
1373
1324
|
}
|
|
1374
1325
|
catch (err) {
|
|
1375
|
-
console.error('[uploadImageFromClipboard] macOS读取剪贴板失败:', err);
|
|
1376
1326
|
return {
|
|
1377
1327
|
content: [{
|
|
1378
1328
|
type: "text",
|
|
@@ -1387,17 +1337,14 @@ server.tool("uploadImageFromClipboard", "上传剪贴板图片到禅道 - 自动
|
|
|
1387
1337
|
}
|
|
1388
1338
|
// Linux: 使用 xclip
|
|
1389
1339
|
else {
|
|
1390
|
-
console.log('[uploadImageFromClipboard] Linux平台,使用xclip读取剪贴板...');
|
|
1391
1340
|
try {
|
|
1392
1341
|
const buffer = execSync('xclip -selection clipboard -t image/png -o', {
|
|
1393
1342
|
maxBuffer: 10 * 1024 * 1024
|
|
1394
1343
|
});
|
|
1395
1344
|
fileBuffer = buffer;
|
|
1396
1345
|
finalFilename = filename || `clipboard_${Date.now()}.png`;
|
|
1397
|
-
console.log(`[uploadImageFromClipboard] 已读取剪贴板图片,大小: ${fileBuffer.length} bytes`);
|
|
1398
1346
|
}
|
|
1399
1347
|
catch (err) {
|
|
1400
|
-
console.error('[uploadImageFromClipboard] Linux读取剪贴板失败:', err);
|
|
1401
1348
|
return {
|
|
1402
1349
|
content: [{
|
|
1403
1350
|
type: "text",
|
|
@@ -1413,21 +1360,17 @@ server.tool("uploadImageFromClipboard", "上传剪贴板图片到禅道 - 自动
|
|
|
1413
1360
|
// 创建 img 文件夹
|
|
1414
1361
|
const imgDir = path.join(process.cwd(), 'img');
|
|
1415
1362
|
if (!fs.existsSync(imgDir)) {
|
|
1416
|
-
console.log(`[uploadImageFromClipboard] 创建目录: ${imgDir}`);
|
|
1417
1363
|
fs.mkdirSync(imgDir, { recursive: true });
|
|
1418
1364
|
}
|
|
1419
1365
|
// 保存到 img 文件夹
|
|
1420
1366
|
const savedPath = path.join(imgDir, finalFilename);
|
|
1421
1367
|
fs.writeFileSync(savedPath, fileBuffer);
|
|
1422
|
-
console.log(`[uploadImageFromClipboard] 图片已保存到: ${savedPath}`);
|
|
1423
1368
|
// 上传到禅道
|
|
1424
|
-
console.log('[uploadImageFromClipboard] 开始上传到禅道...');
|
|
1425
1369
|
const uploadResult = await zentaoApi.uploadFile({
|
|
1426
1370
|
file: fileBuffer,
|
|
1427
1371
|
filename: finalFilename,
|
|
1428
1372
|
uid
|
|
1429
1373
|
});
|
|
1430
|
-
console.log('[uploadImageFromClipboard] 上传成功,结果:', uploadResult);
|
|
1431
1374
|
// 生成禅道需要的 HTML 格式
|
|
1432
1375
|
const fileId = uploadResult.id;
|
|
1433
1376
|
const baseUrl = zentaoApi.getConfig().url;
|
|
@@ -1445,7 +1388,6 @@ server.tool("uploadImageFromClipboard", "上传剪贴板图片到禅道 - 自动
|
|
|
1445
1388
|
message: `图片已保存到本地并上传到禅道`,
|
|
1446
1389
|
tip: `更新 Bug 描述时,请使用 imageHtml 字段中的 HTML 代码。图片会被包裹在 <p> 标签中以确保正确显示。`
|
|
1447
1390
|
};
|
|
1448
|
-
console.log('[uploadImageFromClipboard] 返回结果:', response);
|
|
1449
1391
|
return {
|
|
1450
1392
|
content: [{
|
|
1451
1393
|
type: "text",
|
|
@@ -1454,7 +1396,6 @@ server.tool("uploadImageFromClipboard", "上传剪贴板图片到禅道 - 自动
|
|
|
1454
1396
|
};
|
|
1455
1397
|
}
|
|
1456
1398
|
catch (error) {
|
|
1457
|
-
console.error('[uploadImageFromClipboard] 发生错误:', error);
|
|
1458
1399
|
const errorResponse = {
|
|
1459
1400
|
success: false,
|
|
1460
1401
|
error: error.message || String(error),
|
|
@@ -1601,4 +1542,7 @@ server.tool("deleteComment", "删除评论 - 只能删除自己的评论,管
|
|
|
1601
1542
|
});
|
|
1602
1543
|
// Start receiving messages on stdin and sending messages on stdout
|
|
1603
1544
|
const transport = new StdioServerTransport();
|
|
1604
|
-
await server.connect(transport).catch(
|
|
1545
|
+
await server.connect(transport).catch(err => {
|
|
1546
|
+
process.stderr.write('[FATAL] MCP server failed: ' + String(err) + '\n');
|
|
1547
|
+
process.exit(1);
|
|
1548
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { ZentaoAPI } from "./api/zentaoApi.js";
|
|
3
|
+
export type ApiGetter = () => ZentaoAPI | null;
|
|
4
|
+
export declare function ensureApi(getApi: ApiGetter): ZentaoAPI;
|
|
5
|
+
export declare function withApi<TArgs, TResult>(getApi: ApiGetter, handler: (api: ZentaoAPI, args: TArgs) => Promise<TResult>): (args: TArgs) => Promise<TResult>;
|
|
6
|
+
export type ToolHandler<TArgs = any> = (args: TArgs) => Promise<any>;
|
|
7
|
+
export interface ToolDefinition<TArgs = any> {
|
|
8
|
+
name: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
schema: any;
|
|
11
|
+
handler: ToolHandler<TArgs>;
|
|
12
|
+
}
|
|
13
|
+
export declare function registerTools(server: McpServer, tools: ToolDefinition[]): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function ensureApi(getApi) {
|
|
2
|
+
const api = getApi();
|
|
3
|
+
if (!api) {
|
|
4
|
+
throw new Error("Please initialize Zentao API first");
|
|
5
|
+
}
|
|
6
|
+
return api;
|
|
7
|
+
}
|
|
8
|
+
export function withApi(getApi, handler) {
|
|
9
|
+
return async (args) => {
|
|
10
|
+
const api = ensureApi(getApi);
|
|
11
|
+
return handler(api, args);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function registerTools(server, tools) {
|
|
15
|
+
for (const tool of tools) {
|
|
16
|
+
const { name, description, schema, handler } = tool;
|
|
17
|
+
if (description) {
|
|
18
|
+
server.tool(name, description, schema, handler);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
server.tool(name, schema, handler);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|