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.
Files changed (29) hide show
  1. package/README.md +819 -773
  2. package/build/index.js +222 -134
  3. package/build/lib/__tests__/custom-mcp-resources.unit.test.d.ts +1 -0
  4. package/build/lib/__tests__/custom-mcp-resources.unit.test.js +135 -0
  5. package/build/lib/__tests__/project-mcp-resources.unit.test.d.ts +1 -0
  6. package/build/lib/__tests__/project-mcp-resources.unit.test.js +49 -0
  7. package/build/lib/__tests__/workspace-root.unit.test.d.ts +1 -0
  8. package/build/lib/__tests__/workspace-root.unit.test.js +63 -0
  9. package/build/lib/custom-mcp-resources.d.ts +39 -0
  10. package/build/lib/custom-mcp-resources.js +222 -0
  11. package/build/lib/output-schema-registry.d.ts +8 -0
  12. package/build/lib/output-schema-registry.js +14 -0
  13. package/build/lib/project-mcp-resources.d.ts +61 -0
  14. package/build/lib/project-mcp-resources.js +173 -0
  15. package/build/lib/workflow-skill-installer.d.ts +2 -0
  16. package/build/lib/workflow-skill-installer.js +15 -1
  17. package/build/lib/workflow-skill-template.js +1 -1
  18. package/build/lib/workspace-root.d.ts +21 -0
  19. package/build/lib/workspace-root.js +161 -21
  20. package/build/schemas/basic-tools.d.ts +4 -0
  21. package/build/schemas/basic-tools.js +5 -0
  22. package/build/schemas/index.d.ts +5 -1
  23. package/build/schemas/output/project-tools.d.ts +52 -0
  24. package/build/schemas/output/project-tools.js +16 -0
  25. package/build/schemas/project-tools.d.ts +1 -1
  26. package/build/schemas/project-tools.js +2 -1
  27. package/build/tools/init_project.js +201 -158
  28. package/build/tools/scan_and_extract_patterns.js +1 -1
  29. 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 { withToolAnnotations } from "./lib/tool-annotations.js";
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 decorateResult(toolName, args, raw, traceMeta) {
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
- console.error(`[MCP Probe Kit] 当前工具集: ${toolset} (${filteredTools.length}/${allToolSchemas.length} 个工具)`);
357
- return {
358
- tools: filteredTools.map((tool) => withOutputSchema(withToolAnnotations(tool))),
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
- return await initProject(args);
452
+ result = (await initProject(args));
453
+ break;
378
454
  case "gencommit":
379
- return await gencommit(args);
455
+ result = (await gencommit(args));
456
+ break;
380
457
  case "code_review":
381
- return await codeReview(args);
458
+ result = (await codeReview(args));
459
+ break;
382
460
  case "code_insight":
383
- return await codeInsight(args, context);
461
+ result = (await codeInsight(args, context));
462
+ break;
384
463
  case "gentest":
385
- return await gentest(args);
464
+ result = (await gentest(args));
465
+ break;
386
466
  case "refactor":
387
- return await refactor(args);
467
+ result = (await refactor(args));
468
+ break;
388
469
  case "init_project_context":
389
- return await initProjectContext(args);
470
+ result = (await initProjectContext(args));
471
+ break;
390
472
  case "workflow":
391
- return await workflow(args);
473
+ result = (await workflow(args));
474
+ break;
392
475
  case "add_feature":
393
- return await addFeature(args);
476
+ result = (await addFeature(args));
477
+ break;
394
478
  case "check_spec":
395
- return await checkSpec(args);
479
+ result = (await checkSpec(args));
480
+ break;
396
481
  case "fix_bug":
397
- return await fixBug(args);
482
+ result = (await fixBug(args));
483
+ break;
398
484
  case "estimate":
399
- return await estimate(args);
485
+ result = (await estimate(args));
486
+ break;
400
487
  case "start_feature":
401
- return await startFeature(args, context);
488
+ result = (await startFeature(args, context));
489
+ break;
402
490
  case "start_bugfix":
403
- return await startBugfix(args, context);
491
+ result = (await startBugfix(args, context));
492
+ break;
404
493
  case "start_onboard":
405
- return await startOnboard(args, context);
494
+ result = (await startOnboard(args, context));
495
+ break;
406
496
  case "start_ralph":
407
- return await startRalph(args, context);
497
+ result = (await startRalph(args, context));
498
+ break;
408
499
  case "interview":
409
- return await interview(args);
500
+ result = (await interview(args));
501
+ break;
410
502
  case "ask_user":
411
- return await askUser(args);
503
+ result = (await askUser(args));
504
+ break;
412
505
  case "ui_design_system":
413
- return await uiDesignSystem(args);
506
+ result = (await uiDesignSystem(args));
507
+ break;
414
508
  case "ui_search":
415
- return await uiSearch(args);
509
+ result = (await uiSearch(args));
510
+ break;
416
511
  case "sync_ui_data":
417
- return await syncUiData(args, context);
512
+ result = (await syncUiData(args, context));
513
+ break;
418
514
  case "start_ui":
419
- return await startUi(args, context);
515
+ result = (await startUi(args, context));
516
+ break;
420
517
  case "start_product":
421
- return await startProduct((args ?? {}), context);
518
+ result = (await startProduct((args ?? {}), context));
519
+ break;
422
520
  case "git_work_report":
423
- return await gitWorkReport(args);
521
+ result = (await gitWorkReport(args));
522
+ break;
424
523
  case "search_memory":
425
- return await searchMemory(args);
524
+ result = (await searchMemory(args));
525
+ break;
426
526
  case "read_memory_asset":
427
- return await readMemoryAsset(args);
527
+ result = (await readMemoryAsset(args));
528
+ break;
428
529
  case "memorize_asset":
429
- return await memorizeAsset(args);
530
+ result = (await memorizeAsset(args));
531
+ break;
430
532
  case "delete_memory_asset":
431
- return await deleteMemoryAsset(args);
533
+ result = (await deleteMemoryAsset(args));
534
+ break;
432
535
  case "update_memory_asset":
433
- return await updateMemoryAsset(args);
536
+ result = (await updateMemoryAsset(args));
537
+ break;
434
538
  case "scan_and_extract_patterns":
435
- return await scanAndExtractPatterns(args);
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: "probe://graph/latest",
617
- name: "图谱快照(最新)",
618
- description: "最近一次 code_insight start_* 生成的图谱快照",
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
- for (const id of graphSnapshotOrder.slice().reverse().slice(0, 10)) {
641
- const snapshot = graphSnapshots.get(id);
642
- if (!snapshot) {
643
- continue;
644
- }
645
- resources.push({
646
- uri: snapshot.uri,
647
- name: `图谱快照 · ${snapshot.toolName}`,
648
- description: `${snapshot.status} · ${trimText(snapshot.summary, 120)} (${snapshot.createdAt})`,
649
- mimeType: "application/json",
650
- });
651
- }
652
- if (uiAppsEnabled) {
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 = graphSnapshotOrder
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
- console.error("MCP Probe Kit 服务器已启动");
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,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
+ });