mcp-probe-kit 3.6.0 → 3.6.2
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 +819 -773
- package/build/index.js +222 -134
- package/build/lib/__tests__/custom-mcp-resources.unit.test.d.ts +1 -0
- package/build/lib/__tests__/custom-mcp-resources.unit.test.js +135 -0
- package/build/lib/__tests__/project-mcp-resources.unit.test.d.ts +1 -0
- package/build/lib/__tests__/project-mcp-resources.unit.test.js +49 -0
- package/build/lib/__tests__/workspace-root.unit.test.d.ts +1 -0
- package/build/lib/__tests__/workspace-root.unit.test.js +63 -0
- package/build/lib/custom-mcp-resources.d.ts +39 -0
- package/build/lib/custom-mcp-resources.js +222 -0
- package/build/lib/output-schema-registry.d.ts +8 -0
- package/build/lib/output-schema-registry.js +14 -0
- package/build/lib/project-mcp-resources.d.ts +61 -0
- package/build/lib/project-mcp-resources.js +173 -0
- package/build/lib/workflow-skill-installer.d.ts +2 -0
- package/build/lib/workflow-skill-installer.js +15 -1
- package/build/lib/workflow-skill-template.js +1 -1
- package/build/lib/workspace-root.d.ts +21 -0
- package/build/lib/workspace-root.js +161 -21
- package/build/schemas/basic-tools.d.ts +4 -0
- package/build/schemas/basic-tools.js +5 -0
- package/build/schemas/index.d.ts +5 -1
- package/build/schemas/output/project-tools.d.ts +52 -0
- package/build/schemas/output/project-tools.js +16 -0
- package/build/schemas/project-tools.d.ts +1 -1
- package/build/schemas/project-tools.js +2 -1
- package/build/tools/init_project.js +201 -158
- package/build/tools/scan_and_extract_patterns.js +1 -1
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -9,12 +9,13 @@ import { initProject, gencommit, codeReview, codeInsight, gentest, refactor, ini
|
|
|
9
9
|
import { VERSION, NAME } from "./version.js";
|
|
10
10
|
import { allToolSchemas } from "./schemas/index.js";
|
|
11
11
|
import { filterTools, getToolsetFromEnv } from "./lib/toolset-manager.js";
|
|
12
|
-
import {
|
|
13
|
-
import { withOutputSchema } from "./lib/output-schema-registry.js";
|
|
12
|
+
import { prepareToolForToolsList } from "./lib/output-schema-registry.js";
|
|
14
13
|
import { shouldAutoEscalateToTask } from "./lib/task-defaults.js";
|
|
15
14
|
import { attachHandles } from "./lib/handles.js";
|
|
16
15
|
import { buildMcpAppHtml, isMcpUiAppTool } from "./lib/mcp-apps.js";
|
|
17
|
-
import { ensureMcpProbeKitBootstrapForToolCall } from "./lib/workflow-skill-installer.js";
|
|
16
|
+
import { ensureMcpProbeKitBootstrapForToolCall, } from "./lib/workflow-skill-installer.js";
|
|
17
|
+
import { resolveWorkspaceRoot, resolveWorkspaceRootWithMeta } from "./lib/workspace-root.js";
|
|
18
|
+
import { PROJECT_BOOTSTRAP_URI, discoverProjectResources, readProjectResourceContent, } from "./lib/project-mcp-resources.js";
|
|
18
19
|
import { isAbortError, } from "./lib/tool-execution-context.js";
|
|
19
20
|
const EXTENSIONS_CAPABILITY_KEY = "io.github.mybolide/extensions";
|
|
20
21
|
const MAX_UI_APP_RESOURCES = 30;
|
|
@@ -184,6 +185,53 @@ function renderGraphSnapshotMarkdown(snapshot) {
|
|
|
184
185
|
"",
|
|
185
186
|
].join("\n");
|
|
186
187
|
}
|
|
188
|
+
function buildGraphHistorySummary(summaryLimit = 200) {
|
|
189
|
+
const items = graphSnapshotOrder
|
|
190
|
+
.slice()
|
|
191
|
+
.reverse()
|
|
192
|
+
.map((id) => graphSnapshots.get(id))
|
|
193
|
+
.filter((item) => Boolean(item))
|
|
194
|
+
.map((item) => ({
|
|
195
|
+
id: item.id,
|
|
196
|
+
uri: item.uri,
|
|
197
|
+
toolName: item.toolName,
|
|
198
|
+
createdAt: item.createdAt,
|
|
199
|
+
status: item.status,
|
|
200
|
+
summary: trimText(item.summary, summaryLimit),
|
|
201
|
+
files: {
|
|
202
|
+
json: item.jsonFilePath ?? null,
|
|
203
|
+
markdown: item.markdownFilePath ?? null,
|
|
204
|
+
},
|
|
205
|
+
}));
|
|
206
|
+
return { count: items.length, items };
|
|
207
|
+
}
|
|
208
|
+
function buildGraphFilesIndex(fileLimit = 40) {
|
|
209
|
+
const latestId = graphSnapshotOrder[graphSnapshotOrder.length - 1];
|
|
210
|
+
const latest = latestId ? (graphSnapshots.get(latestId) ?? null) : null;
|
|
211
|
+
const hasDir = fs.existsSync(graphSnapshotDir);
|
|
212
|
+
const files = hasDir
|
|
213
|
+
? fs
|
|
214
|
+
.readdirSync(graphSnapshotDir, { withFileTypes: true })
|
|
215
|
+
.filter((entry) => entry.isFile() && /\.(json|md)$/i.test(entry.name))
|
|
216
|
+
.map((entry) => toPosixPath(path.join(graphSnapshotDir, entry.name)))
|
|
217
|
+
.sort((a, b) => b.localeCompare(a))
|
|
218
|
+
.slice(0, fileLimit)
|
|
219
|
+
: [];
|
|
220
|
+
return {
|
|
221
|
+
snapshotDir: toPosixPath(graphSnapshotDir),
|
|
222
|
+
exists: hasDir,
|
|
223
|
+
latest: latest
|
|
224
|
+
? {
|
|
225
|
+
id: latest.id,
|
|
226
|
+
uri: latest.uri,
|
|
227
|
+
toolName: latest.toolName,
|
|
228
|
+
jsonFilePath: latest.jsonFilePath ?? null,
|
|
229
|
+
markdownFilePath: latest.markdownFilePath ?? null,
|
|
230
|
+
}
|
|
231
|
+
: null,
|
|
232
|
+
files,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
187
235
|
function persistGraphSnapshot(snapshot) {
|
|
188
236
|
try {
|
|
189
237
|
ensureGraphSnapshotDir();
|
|
@@ -324,8 +372,31 @@ function withStructuredHandles(result, handles) {
|
|
|
324
372
|
structuredContent: attachHandles(result.structuredContent, handles),
|
|
325
373
|
};
|
|
326
374
|
}
|
|
327
|
-
function
|
|
375
|
+
function withBootstrapMeta(result, bootstrap) {
|
|
376
|
+
if (!bootstrap) {
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
379
|
+
const base = result.structuredContent &&
|
|
380
|
+
typeof result.structuredContent === "object" &&
|
|
381
|
+
!Array.isArray(result.structuredContent)
|
|
382
|
+
? result.structuredContent
|
|
383
|
+
: {};
|
|
384
|
+
return {
|
|
385
|
+
...result,
|
|
386
|
+
structuredContent: {
|
|
387
|
+
...base,
|
|
388
|
+
mcp_probe_bootstrap: {
|
|
389
|
+
projectRoot: bootstrap.projectRoot,
|
|
390
|
+
skill: bootstrap.skill,
|
|
391
|
+
agentsMd: bootstrap.agentsMd,
|
|
392
|
+
workspaceWarning: bootstrap.workspaceWarning ?? null,
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function decorateResult(toolName, args, raw, traceMeta, bootstrap = null) {
|
|
328
398
|
let result = withTraceMeta(raw, traceMeta);
|
|
399
|
+
result = withBootstrapMeta(result, bootstrap);
|
|
329
400
|
const snapshot = rememberGraphSnapshot(toolName, result);
|
|
330
401
|
if (snapshot) {
|
|
331
402
|
result = withGraphSnapshotMeta(result, snapshot);
|
|
@@ -353,13 +424,16 @@ const server = new Server({
|
|
|
353
424
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
354
425
|
const toolset = getToolsetFromEnv();
|
|
355
426
|
const filteredTools = filterTools(allToolSchemas, toolset);
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
};
|
|
427
|
+
const tools = filteredTools.map((tool) => prepareToolForToolsList(tool));
|
|
428
|
+
const payloadBytes = Buffer.byteLength(JSON.stringify({ tools }), "utf8");
|
|
429
|
+
console.error(`[MCP Probe Kit] 当前工具集: ${toolset} (${tools.length}/${allToolSchemas.length} 个工具) | tools/list ≈ ${(payloadBytes / 1024).toFixed(1)} KB`);
|
|
430
|
+
return { tools };
|
|
360
431
|
});
|
|
361
432
|
async function executeTool(name, args, context) {
|
|
362
433
|
const bootstrap = ensureMcpProbeKitBootstrapForToolCall(name, args);
|
|
434
|
+
if (bootstrap?.workspaceWarning) {
|
|
435
|
+
console.error(`[MCP Probe Kit] ${bootstrap.workspaceWarning}`);
|
|
436
|
+
}
|
|
363
437
|
if (bootstrap?.skill.created) {
|
|
364
438
|
console.error(`[MCP Probe Kit] 已创建 MCP Skill: ${bootstrap.skill.skillRelPath} (v${bootstrap.skill.version})`);
|
|
365
439
|
}
|
|
@@ -372,70 +446,102 @@ async function executeTool(name, args, context) {
|
|
|
372
446
|
else if (bootstrap?.agentsMd.updated) {
|
|
373
447
|
console.error(`[MCP Probe Kit] 已更新 AGENTS.md(添加 Skill 引用): ${bootstrap.agentsMd.path}`);
|
|
374
448
|
}
|
|
449
|
+
let result;
|
|
375
450
|
switch (name) {
|
|
376
451
|
case "init_project":
|
|
377
|
-
|
|
452
|
+
result = (await initProject(args));
|
|
453
|
+
break;
|
|
378
454
|
case "gencommit":
|
|
379
|
-
|
|
455
|
+
result = (await gencommit(args));
|
|
456
|
+
break;
|
|
380
457
|
case "code_review":
|
|
381
|
-
|
|
458
|
+
result = (await codeReview(args));
|
|
459
|
+
break;
|
|
382
460
|
case "code_insight":
|
|
383
|
-
|
|
461
|
+
result = (await codeInsight(args, context));
|
|
462
|
+
break;
|
|
384
463
|
case "gentest":
|
|
385
|
-
|
|
464
|
+
result = (await gentest(args));
|
|
465
|
+
break;
|
|
386
466
|
case "refactor":
|
|
387
|
-
|
|
467
|
+
result = (await refactor(args));
|
|
468
|
+
break;
|
|
388
469
|
case "init_project_context":
|
|
389
|
-
|
|
470
|
+
result = (await initProjectContext(args));
|
|
471
|
+
break;
|
|
390
472
|
case "workflow":
|
|
391
|
-
|
|
473
|
+
result = (await workflow(args));
|
|
474
|
+
break;
|
|
392
475
|
case "add_feature":
|
|
393
|
-
|
|
476
|
+
result = (await addFeature(args));
|
|
477
|
+
break;
|
|
394
478
|
case "check_spec":
|
|
395
|
-
|
|
479
|
+
result = (await checkSpec(args));
|
|
480
|
+
break;
|
|
396
481
|
case "fix_bug":
|
|
397
|
-
|
|
482
|
+
result = (await fixBug(args));
|
|
483
|
+
break;
|
|
398
484
|
case "estimate":
|
|
399
|
-
|
|
485
|
+
result = (await estimate(args));
|
|
486
|
+
break;
|
|
400
487
|
case "start_feature":
|
|
401
|
-
|
|
488
|
+
result = (await startFeature(args, context));
|
|
489
|
+
break;
|
|
402
490
|
case "start_bugfix":
|
|
403
|
-
|
|
491
|
+
result = (await startBugfix(args, context));
|
|
492
|
+
break;
|
|
404
493
|
case "start_onboard":
|
|
405
|
-
|
|
494
|
+
result = (await startOnboard(args, context));
|
|
495
|
+
break;
|
|
406
496
|
case "start_ralph":
|
|
407
|
-
|
|
497
|
+
result = (await startRalph(args, context));
|
|
498
|
+
break;
|
|
408
499
|
case "interview":
|
|
409
|
-
|
|
500
|
+
result = (await interview(args));
|
|
501
|
+
break;
|
|
410
502
|
case "ask_user":
|
|
411
|
-
|
|
503
|
+
result = (await askUser(args));
|
|
504
|
+
break;
|
|
412
505
|
case "ui_design_system":
|
|
413
|
-
|
|
506
|
+
result = (await uiDesignSystem(args));
|
|
507
|
+
break;
|
|
414
508
|
case "ui_search":
|
|
415
|
-
|
|
509
|
+
result = (await uiSearch(args));
|
|
510
|
+
break;
|
|
416
511
|
case "sync_ui_data":
|
|
417
|
-
|
|
512
|
+
result = (await syncUiData(args, context));
|
|
513
|
+
break;
|
|
418
514
|
case "start_ui":
|
|
419
|
-
|
|
515
|
+
result = (await startUi(args, context));
|
|
516
|
+
break;
|
|
420
517
|
case "start_product":
|
|
421
|
-
|
|
518
|
+
result = (await startProduct((args ?? {}), context));
|
|
519
|
+
break;
|
|
422
520
|
case "git_work_report":
|
|
423
|
-
|
|
521
|
+
result = (await gitWorkReport(args));
|
|
522
|
+
break;
|
|
424
523
|
case "search_memory":
|
|
425
|
-
|
|
524
|
+
result = (await searchMemory(args));
|
|
525
|
+
break;
|
|
426
526
|
case "read_memory_asset":
|
|
427
|
-
|
|
527
|
+
result = (await readMemoryAsset(args));
|
|
528
|
+
break;
|
|
428
529
|
case "memorize_asset":
|
|
429
|
-
|
|
530
|
+
result = (await memorizeAsset(args));
|
|
531
|
+
break;
|
|
430
532
|
case "delete_memory_asset":
|
|
431
|
-
|
|
533
|
+
result = (await deleteMemoryAsset(args));
|
|
534
|
+
break;
|
|
432
535
|
case "update_memory_asset":
|
|
433
|
-
|
|
536
|
+
result = (await updateMemoryAsset(args));
|
|
537
|
+
break;
|
|
434
538
|
case "scan_and_extract_patterns":
|
|
435
|
-
|
|
539
|
+
result = (await scanAndExtractPatterns(args));
|
|
540
|
+
break;
|
|
436
541
|
default:
|
|
437
542
|
throw new Error(`未知工具: ${name}`);
|
|
438
543
|
}
|
|
544
|
+
return { bootstrap, result };
|
|
439
545
|
}
|
|
440
546
|
function makeToolError(errorMessage) {
|
|
441
547
|
return {
|
|
@@ -528,11 +634,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
|
528
634
|
void (async () => {
|
|
529
635
|
try {
|
|
530
636
|
await taskContext.reportProgress?.(5, `开始执行工具: ${name}`);
|
|
531
|
-
const rawResult = await executeTool(name, args, taskContext);
|
|
637
|
+
const { bootstrap, result: rawResult } = await executeTool(name, args, taskContext);
|
|
532
638
|
if (!rawResult || typeof rawResult !== "object") {
|
|
533
639
|
throw new Error(`工具 ${name} 返回了无效响应`);
|
|
534
640
|
}
|
|
535
|
-
const result = decorateResult(name, args, rawResult, traceMeta);
|
|
641
|
+
const result = decorateResult(name, args, rawResult, traceMeta, bootstrap);
|
|
536
642
|
const latestTask = await extra.taskStore?.getTask(task.taskId);
|
|
537
643
|
if (!latestTask || isTerminalTaskStatus(latestTask.status)) {
|
|
538
644
|
return;
|
|
@@ -583,12 +689,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
|
583
689
|
try {
|
|
584
690
|
ensureNotAborted();
|
|
585
691
|
await emitProgress(5, `开始执行工具: ${name}`);
|
|
586
|
-
const rawResult = await executeTool(name, args, toolContext);
|
|
692
|
+
const { bootstrap, result: rawResult } = await executeTool(name, args, toolContext);
|
|
587
693
|
if (!rawResult || typeof rawResult !== "object") {
|
|
588
694
|
throw new Error(`工具 ${name} 返回了无效响应`);
|
|
589
695
|
}
|
|
590
696
|
ensureNotAborted();
|
|
591
|
-
const result = decorateResult(name, args, rawResult, traceMeta);
|
|
697
|
+
const result = decorateResult(name, args, rawResult, traceMeta, bootstrap);
|
|
592
698
|
await emitProgress(100, `工具执行完成: ${name}`);
|
|
593
699
|
return result;
|
|
594
700
|
}
|
|
@@ -603,7 +709,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
|
603
709
|
return withTraceMeta(makeToolError(errorMessage), traceMeta);
|
|
604
710
|
}
|
|
605
711
|
});
|
|
606
|
-
//
|
|
712
|
+
// 定义资源列表(轻量只读;bootstrap 写盘仅在 resources/read probe://project/bootstrap 或工具调用时)
|
|
607
713
|
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
608
714
|
const resources = [
|
|
609
715
|
{
|
|
@@ -613,56 +719,32 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
|
613
719
|
mimeType: "application/json",
|
|
614
720
|
},
|
|
615
721
|
{
|
|
616
|
-
uri:
|
|
617
|
-
name: "
|
|
618
|
-
description: "
|
|
619
|
-
mimeType: "application/json",
|
|
620
|
-
},
|
|
621
|
-
{
|
|
622
|
-
uri: "probe://graph/history",
|
|
623
|
-
name: "图谱快照(历史)",
|
|
624
|
-
description: `最近 ${graphSnapshotOrder.length} 条图谱快照摘要`,
|
|
625
|
-
mimeType: "application/json",
|
|
626
|
-
},
|
|
627
|
-
{
|
|
628
|
-
uri: "probe://graph/latest.md",
|
|
629
|
-
name: "图谱快照(最新 Markdown)",
|
|
630
|
-
description: "最近一次图谱快照的 Markdown 视图",
|
|
631
|
-
mimeType: "text/markdown",
|
|
632
|
-
},
|
|
633
|
-
{
|
|
634
|
-
uri: "probe://graph/files",
|
|
635
|
-
name: "图谱快照(文件索引)",
|
|
636
|
-
description: `图谱快照落盘目录: ${toPosixPath(graphSnapshotDir)}`,
|
|
722
|
+
uri: PROJECT_BOOTSTRAP_URI,
|
|
723
|
+
name: "项目 MCP 自检",
|
|
724
|
+
description: "读取时自动补齐 Skill + AGENTS.md,并返回 probe://project/skill|agents|context|graph 入口",
|
|
637
725
|
mimeType: "application/json",
|
|
638
726
|
},
|
|
639
727
|
];
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
for (const uri of uiAppResourceOrder.slice().reverse()) {
|
|
654
|
-
const entry = uiAppResources.get(uri);
|
|
655
|
-
if (!entry) {
|
|
656
|
-
continue;
|
|
728
|
+
try {
|
|
729
|
+
if (uiAppsEnabled) {
|
|
730
|
+
for (const uri of uiAppResourceOrder.slice().reverse()) {
|
|
731
|
+
const entry = uiAppResources.get(uri);
|
|
732
|
+
if (!entry) {
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
resources.push({
|
|
736
|
+
uri: entry.uri,
|
|
737
|
+
name: entry.name,
|
|
738
|
+
description: `${entry.description} (${entry.createdAt})`,
|
|
739
|
+
mimeType: entry.mimeType,
|
|
740
|
+
});
|
|
657
741
|
}
|
|
658
|
-
resources.push({
|
|
659
|
-
uri: entry.uri,
|
|
660
|
-
name: entry.name,
|
|
661
|
-
description: `${entry.description} (${entry.createdAt})`,
|
|
662
|
-
mimeType: entry.mimeType,
|
|
663
|
-
});
|
|
664
742
|
}
|
|
665
743
|
}
|
|
744
|
+
catch (error) {
|
|
745
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
746
|
+
console.error(`[MCP Probe Kit] resources/list UI 资源合并失败: ${message}`);
|
|
747
|
+
}
|
|
666
748
|
return {
|
|
667
749
|
resources,
|
|
668
750
|
};
|
|
@@ -719,11 +801,49 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
719
801
|
})(),
|
|
720
802
|
},
|
|
721
803
|
toolCount: allToolSchemas.length,
|
|
804
|
+
projectResources: (() => {
|
|
805
|
+
try {
|
|
806
|
+
const discovered = discoverProjectResources(resolveWorkspaceRoot(""));
|
|
807
|
+
return {
|
|
808
|
+
bootstrapUri: PROJECT_BOOTSTRAP_URI,
|
|
809
|
+
projectRoot: toPosixPath(discovered.projectRoot),
|
|
810
|
+
available: discovered.resources
|
|
811
|
+
.filter((item) => item.exists)
|
|
812
|
+
.map((item) => item.uri),
|
|
813
|
+
note: "读取 probe://project/bootstrap 时执行 Skill/AGENTS 自动补齐",
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
catch (error) {
|
|
817
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
818
|
+
return { bootstrapUri: PROJECT_BOOTSTRAP_URI, error: message };
|
|
819
|
+
}
|
|
820
|
+
})(),
|
|
722
821
|
}, null, 2),
|
|
723
822
|
},
|
|
724
823
|
],
|
|
725
824
|
};
|
|
726
825
|
}
|
|
826
|
+
if (uri === PROJECT_BOOTSTRAP_URI || uri.startsWith("probe://project/")) {
|
|
827
|
+
try {
|
|
828
|
+
const content = readProjectResourceContent(uri, resolveWorkspaceRoot(""));
|
|
829
|
+
if (!content) {
|
|
830
|
+
throw new Error(`未知项目 resource: ${uri}`);
|
|
831
|
+
}
|
|
832
|
+
return {
|
|
833
|
+
contents: [
|
|
834
|
+
{
|
|
835
|
+
uri: content.uri,
|
|
836
|
+
mimeType: content.mimeType,
|
|
837
|
+
text: content.text,
|
|
838
|
+
},
|
|
839
|
+
],
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
catch (error) {
|
|
843
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
844
|
+
throw new Error(`读取项目 resource 失败 (${uri}): ${message}`);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
727
847
|
if (uri.startsWith("ui://")) {
|
|
728
848
|
const entry = uiAppResources.get(uri);
|
|
729
849
|
if (!entry) {
|
|
@@ -776,6 +896,13 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
776
896
|
json: snapshot.jsonFilePath ?? null,
|
|
777
897
|
markdown: snapshot.markdownFilePath ?? null,
|
|
778
898
|
},
|
|
899
|
+
history: buildGraphHistorySummary(),
|
|
900
|
+
fileIndex: buildGraphFilesIndex(),
|
|
901
|
+
relatedUris: {
|
|
902
|
+
markdown: "probe://graph/latest.md",
|
|
903
|
+
history: "probe://graph/history",
|
|
904
|
+
files: "probe://graph/files",
|
|
905
|
+
},
|
|
779
906
|
}, null, 2),
|
|
780
907
|
},
|
|
781
908
|
],
|
|
@@ -812,67 +939,24 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
812
939
|
};
|
|
813
940
|
}
|
|
814
941
|
if (uri === "probe://graph/history") {
|
|
815
|
-
const history =
|
|
816
|
-
.slice()
|
|
817
|
-
.reverse()
|
|
818
|
-
.map((id) => graphSnapshots.get(id))
|
|
819
|
-
.filter((item) => Boolean(item))
|
|
820
|
-
.map((item) => ({
|
|
821
|
-
id: item.id,
|
|
822
|
-
uri: item.uri,
|
|
823
|
-
toolName: item.toolName,
|
|
824
|
-
createdAt: item.createdAt,
|
|
825
|
-
status: item.status,
|
|
826
|
-
summary: trimText(item.summary, 200),
|
|
827
|
-
files: {
|
|
828
|
-
json: item.jsonFilePath ?? null,
|
|
829
|
-
markdown: item.markdownFilePath ?? null,
|
|
830
|
-
},
|
|
831
|
-
}));
|
|
942
|
+
const history = buildGraphHistorySummary();
|
|
832
943
|
return {
|
|
833
944
|
contents: [
|
|
834
945
|
{
|
|
835
946
|
uri,
|
|
836
947
|
mimeType: "application/json",
|
|
837
|
-
text: JSON.stringify(
|
|
838
|
-
count: history.length,
|
|
839
|
-
items: history,
|
|
840
|
-
}, null, 2),
|
|
948
|
+
text: JSON.stringify(history, null, 2),
|
|
841
949
|
},
|
|
842
950
|
],
|
|
843
951
|
};
|
|
844
952
|
}
|
|
845
953
|
if (uri === "probe://graph/files") {
|
|
846
|
-
const latestId = graphSnapshotOrder[graphSnapshotOrder.length - 1];
|
|
847
|
-
const latest = latestId ? graphSnapshots.get(latestId) ?? null : null;
|
|
848
|
-
const hasDir = fs.existsSync(graphSnapshotDir);
|
|
849
|
-
const files = hasDir
|
|
850
|
-
? fs
|
|
851
|
-
.readdirSync(graphSnapshotDir, { withFileTypes: true })
|
|
852
|
-
.filter((entry) => entry.isFile() && /\.(json|md)$/i.test(entry.name))
|
|
853
|
-
.map((entry) => toPosixPath(path.join(graphSnapshotDir, entry.name)))
|
|
854
|
-
.sort((a, b) => b.localeCompare(a))
|
|
855
|
-
.slice(0, 40)
|
|
856
|
-
: [];
|
|
857
954
|
return {
|
|
858
955
|
contents: [
|
|
859
956
|
{
|
|
860
957
|
uri,
|
|
861
958
|
mimeType: "application/json",
|
|
862
|
-
text: JSON.stringify(
|
|
863
|
-
snapshotDir: toPosixPath(graphSnapshotDir),
|
|
864
|
-
exists: hasDir,
|
|
865
|
-
latest: latest
|
|
866
|
-
? {
|
|
867
|
-
id: latest.id,
|
|
868
|
-
uri: latest.uri,
|
|
869
|
-
toolName: latest.toolName,
|
|
870
|
-
jsonFilePath: latest.jsonFilePath ?? null,
|
|
871
|
-
markdownFilePath: latest.markdownFilePath ?? null,
|
|
872
|
-
}
|
|
873
|
-
: null,
|
|
874
|
-
files,
|
|
875
|
-
}, null, 2),
|
|
959
|
+
text: JSON.stringify(buildGraphFilesIndex(), null, 2),
|
|
876
960
|
},
|
|
877
961
|
],
|
|
878
962
|
};
|
|
@@ -914,7 +998,11 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
914
998
|
async function main() {
|
|
915
999
|
const transport = new StdioServerTransport();
|
|
916
1000
|
await server.connect(transport);
|
|
917
|
-
|
|
1001
|
+
const workspaceMeta = resolveWorkspaceRootWithMeta("");
|
|
1002
|
+
console.error(`MCP Probe Kit v${VERSION} 已启动 | workspace=${workspaceMeta.root} | source=${workspaceMeta.source}`);
|
|
1003
|
+
if (workspaceMeta.warning) {
|
|
1004
|
+
console.error(`[MCP Probe Kit] ${workspaceMeta.warning}`);
|
|
1005
|
+
}
|
|
918
1006
|
}
|
|
919
1007
|
// 启动服务器
|
|
920
1008
|
main().catch((error) => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, test } from "vitest";
|
|
5
|
+
import { CUSTOM_RESOURCES_MANIFEST_REL, listCustomResourceDescriptors, loadCustomResourcesManifest, parseCustomResourcesManifest, readCustomResourceContent, resolveCustomResourcesManifestPath, } from "../custom-mcp-resources.js";
|
|
6
|
+
function withTempProject(run) {
|
|
7
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mcp-custom-res-"));
|
|
8
|
+
try {
|
|
9
|
+
run(root);
|
|
10
|
+
}
|
|
11
|
+
finally {
|
|
12
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function writeManifest(root, body) {
|
|
16
|
+
const dir = path.join(root, ".mcp-probe-kit");
|
|
17
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
18
|
+
fs.writeFileSync(path.join(dir, "resources.json"), JSON.stringify(body, null, 2), "utf-8");
|
|
19
|
+
}
|
|
20
|
+
describe("custom-mcp-resources", () => {
|
|
21
|
+
const prevEnv = { ...process.env };
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
process.env = { ...prevEnv };
|
|
24
|
+
});
|
|
25
|
+
test("parseCustomResourcesManifest validates version and uri prefix", () => {
|
|
26
|
+
const entries = parseCustomResourcesManifest(JSON.stringify({
|
|
27
|
+
version: 1,
|
|
28
|
+
resources: [
|
|
29
|
+
{
|
|
30
|
+
uri: "project://guide/readme",
|
|
31
|
+
name: "Guide",
|
|
32
|
+
mimeType: "text/markdown",
|
|
33
|
+
text: "# hello",
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
}));
|
|
37
|
+
expect(entries).toHaveLength(1);
|
|
38
|
+
expect(entries[0]?.uri).toBe("project://guide/readme");
|
|
39
|
+
expect(() => parseCustomResourcesManifest(JSON.stringify({
|
|
40
|
+
version: 1,
|
|
41
|
+
resources: [
|
|
42
|
+
{
|
|
43
|
+
uri: "probe://status",
|
|
44
|
+
name: "Bad",
|
|
45
|
+
mimeType: "text/plain",
|
|
46
|
+
text: "x",
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
}))).toThrow(/project:\/\//);
|
|
50
|
+
});
|
|
51
|
+
test("listCustomResourceDescriptors respects list flag", () => {
|
|
52
|
+
withTempProject((root) => {
|
|
53
|
+
writeManifest(root, {
|
|
54
|
+
version: 1,
|
|
55
|
+
resources: [
|
|
56
|
+
{
|
|
57
|
+
uri: "project://visible",
|
|
58
|
+
name: "Visible",
|
|
59
|
+
mimeType: "text/plain",
|
|
60
|
+
text: "a",
|
|
61
|
+
list: true,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
uri: "project://hidden",
|
|
65
|
+
name: "Hidden",
|
|
66
|
+
mimeType: "text/plain",
|
|
67
|
+
text: "b",
|
|
68
|
+
list: false,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
});
|
|
72
|
+
const listed = listCustomResourceDescriptors(root);
|
|
73
|
+
expect(listed.map((item) => item.uri)).toEqual(["project://visible"]);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
test("readCustomResourceContent reads inline text and file", () => {
|
|
77
|
+
withTempProject((root) => {
|
|
78
|
+
const docsDir = path.join(root, "docs");
|
|
79
|
+
fs.mkdirSync(docsDir, { recursive: true });
|
|
80
|
+
fs.writeFileSync(path.join(docsDir, "note.md"), "# Note", "utf-8");
|
|
81
|
+
writeManifest(root, {
|
|
82
|
+
version: 1,
|
|
83
|
+
resources: [
|
|
84
|
+
{
|
|
85
|
+
uri: "project://inline",
|
|
86
|
+
name: "Inline",
|
|
87
|
+
mimeType: "text/plain",
|
|
88
|
+
text: "inline-body",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
uri: "project://file/note",
|
|
92
|
+
name: "File",
|
|
93
|
+
mimeType: "text/markdown",
|
|
94
|
+
file: "docs/note.md",
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
expect(readCustomResourceContent("project://inline", root)?.text).toBe("inline-body");
|
|
99
|
+
expect(readCustomResourceContent("project://file/note", root)?.text).toBe("# Note");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
test("readCustomResourceContent rejects path traversal", () => {
|
|
103
|
+
withTempProject((root) => {
|
|
104
|
+
writeManifest(root, {
|
|
105
|
+
version: 1,
|
|
106
|
+
resources: [
|
|
107
|
+
{
|
|
108
|
+
uri: "project://escape",
|
|
109
|
+
name: "Escape",
|
|
110
|
+
mimeType: "text/plain",
|
|
111
|
+
file: "../outside.txt",
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
expect(() => readCustomResourceContent("project://escape", root)).toThrow(/\.\./);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
test("loadCustomResourcesManifest returns empty when manifest missing", () => {
|
|
119
|
+
withTempProject((root) => {
|
|
120
|
+
const result = loadCustomResourcesManifest(root);
|
|
121
|
+
expect(result.entries).toEqual([]);
|
|
122
|
+
expect(result.error).toBeNull();
|
|
123
|
+
expect(result.manifestPath).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
test("resolveCustomResourcesManifestPath honors MCP_CUSTOM_RESOURCES_PATH", () => {
|
|
127
|
+
withTempProject((root) => {
|
|
128
|
+
const custom = path.join(root, "cfg", "my-resources.json");
|
|
129
|
+
process.env.MCP_CUSTOM_RESOURCES_PATH = "cfg/my-resources.json";
|
|
130
|
+
expect(resolveCustomResourcesManifestPath(root)).toBe(custom);
|
|
131
|
+
delete process.env.MCP_CUSTOM_RESOURCES_PATH;
|
|
132
|
+
expect(resolveCustomResourcesManifestPath(root)).toBe(path.join(root, CUSTOM_RESOURCES_MANIFEST_REL));
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|