mcp-probe-kit 3.6.1 → 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 CHANGED
@@ -87,8 +87,8 @@ All hard quality rules live in one module (`src/lib/quality-constraints.ts`) and
87
87
  - Older projects that already have `project-context.md` but no graph docs are bootstrapped automatically through the `init_project_context` step
88
88
  - If GitNexus is unavailable, the server falls back automatically without breaking orchestration
89
89
  - Real graph queries read the `.gitnexus` index; `docs/graph-insights/latest.md|json` are readable snapshots for humans and AI agents
90
- - Graph snapshots are exposed as MCP resources: Cursor lists **2 entries** (`probe://status`, `probe://graph/latest`); `latest` embeds history and file index. On-demand URIs (`probe://graph/history`, `probe://graph/latest.md`, `probe://graph/files`) remain readable via `resources/read`
91
- - Graph snapshots are also persisted to readable files in `.mcp-probe-kit/graph-snapshots` (customizable via `MCP_GRAPH_SNAPSHOT_DIR`)
90
+ - MCP resources in MCP client settings list **2 entries** (`probe://status`, `probe://project/bootstrap`). Graph runtime snapshots (`probe://graph/latest`, etc.) and `probe://project/skill|agents|context|graph` remain readable via `resources/read` when tools expose URIs
91
+ - Graph snapshots are persisted to `.mcp-probe-kit/graph-snapshots` (customizable via `MCP_GRAPH_SNAPSHOT_DIR`)
92
92
  - Tool responses include `_meta.graph` with snapshot URI and local JSON/Markdown file paths
93
93
 
94
94
  ### 🐛 TBP 8-Step RCA for Bug Workflows
@@ -415,16 +415,13 @@ No installation needed, use the latest version directly.
415
415
  "mcpServers": {
416
416
  "mcp-probe-kit": {
417
417
  "command": "npx",
418
- "args": ["-y", "mcp-probe-kit@latest"],
419
- "env": {
420
- "MCP_PROJECT_ROOT": "${workspaceFolder}"
421
- }
418
+ "args": ["-y", "mcp-probe-kit@latest"]
422
419
  }
423
420
  }
424
421
  }
425
422
  ```
426
423
 
427
- > **Skill 自动安装**:任意 MCP 工具调用会在用户项目写入 `.agents/skills/mcp-probe-kit/SKILL.md`。务必设置 `MCP_PROJECT_ROOT`(或 Cursor 注入的 `WORKSPACE_FOLDER_PATHS`),否则可能写到错误目录。需 **v3.6.1+**。
424
+ > **Skill 自动安装**:任意 MCP 工具调用会在用户项目写入 `.agents/skills/mcp-probe-kit/SKILL.md`。工作区根目录**自动识别**(Cursor 注入 `WORKSPACE_FOLDER_PATHS`;OpenCode 等项目级 `opencode.json` 会设置进程 cwd),**无需**在每台客户端配置 `MCP_PROJECT_ROOT`。仅全局 MCP 且无法识别工作区时,可选手动设置 `MCP_PROJECT_ROOT` 或在工具参数传 `project_root`。需 **v3.6.2+**。
428
425
 
429
426
  #### Claude Desktop Configuration
430
427
 
@@ -439,10 +436,7 @@ No installation needed, use the latest version directly.
439
436
  "mcpServers": {
440
437
  "mcp-probe-kit": {
441
438
  "command": "npx",
442
- "args": ["-y", "mcp-probe-kit@latest"],
443
- "env": {
444
- "MCP_PROJECT_ROOT": "${workspaceFolder}"
445
- }
439
+ "args": ["-y", "mcp-probe-kit@latest"]
446
440
  }
447
441
  }
448
442
  }
@@ -706,6 +700,50 @@ npx -y mcp-probe-kit@latest 2>&1 | tee ./mcp-probe-kit.log
706
700
  3. Confirm JSON format is correct, no syntax errors
707
701
  4. Check client developer tools or logs for error messages
708
702
 
703
+ ### Q2b: Cursor shows connected but **0 tools** / Agent says **No MCP servers available**?
704
+
705
+ This is a known [Cursor-side issue](https://forum.cursor.com/t/mcp-server-connected-green-dot-and-tools-discovered-in-logs-but-0-tools-in-ui-and-agent/160620): stderr may log `tools/list` with 30 tools, while **Mcp FileSystem Writer** shows `lease returned 0 tools` and `toolCount=0` — the Agent lease layer silently dropped the tool list.
706
+
707
+ **Common causes:**
708
+
709
+ | Symptom in logs | Likely cause |
710
+ |-----------------|--------------|
711
+ | `tools/list ≈ 50+ KB` then `lease returned 0 tools` | Cursor internal payload size limit (whole list dropped silently) |
712
+ | `latched shared-process MCP routing disabled` + `ipcReady` timeout | Windows `mcpProcess` utility failed; legacy fallback discovers tools but Agent lease stays empty |
713
+ | Settings green dot, Agent `No MCP servers available` | Renderer ↔ shared-process MCP routing not wired for this session |
714
+
715
+ **What we do (v3.6.2+):** `tools/list` **omits `outputSchema` by default** (~50 KB → ~23 KB). Structured output still works via `structuredContent` on `tools/call`. To restore full schemas: `MCP_INCLUDE_OUTPUT_SCHEMA=1`.
716
+
717
+ **What you can try:**
718
+
719
+ 1. **Reload MCP** or fully quit Cursor (not just close window) and reopen
720
+ 2. Check **Output → MCP** for `lease returned 0 tools` / `ipcReady` / `MessagePort`
721
+ 3. In **Composer**, open the tools panel — ensure the server toggle is on (some versions default off)
722
+ 4. Upgrade Cursor (3.7.36+ had Windows `ipcReady` regressions; try latest or roll back to a known-good build)
723
+ 5. If still broken after server update, report to Cursor with: `connected=true`, stderr tool count, lease `toolCount=0`, and `shared-process MCP routing disabled`
724
+
725
+ **Diagnostic: `.cursor/projects/<project>/mcps/user-mcp-probe-kit/`**
726
+
727
+ This folder is **written by Cursor** (Mcp FileSystem Writer), not by mcp-probe-kit. After a successful tool lease you should see:
728
+
729
+ ```text
730
+ mcps/user-mcp-probe-kit/
731
+ ├── SERVER_METADATA.json
732
+ ├── STATUS.md
733
+ ├── tools/ ← one JSON per tool (~30); Agent reads these for CallMcpTool
734
+ │ ├── init_project.json
735
+ │ └── ...
736
+ └── resources/ ← from resources/list (may exist even when tools/ is empty)
737
+ ```
738
+
739
+ | State | Meaning |
740
+ |-------|---------|
741
+ | `resources/` exists, `tools/` missing or empty | `resources/list` OK but **tools lease failed** (matches `lease returned 0 tools`) |
742
+ | `tools/` has some files but not 30 | Partial write or session interrupted; Reload MCP |
743
+ | `STATUS.md` says server errored | Cursor marked the server unhealthy for Agent even if Settings is green |
744
+
745
+ Healthy session: `tools/` should auto-populate within seconds of MCP connect — no manual setup, no repo config.
746
+
709
747
  ### Q3: How to update to latest version?
710
748
 
711
749
  **npx method (Recommended):**
package/build/index.js CHANGED
@@ -9,13 +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
16
  import { ensureMcpProbeKitBootstrapForToolCall, } from "./lib/workflow-skill-installer.js";
18
- import { resolveWorkspaceRoot } from "./lib/workspace-root.js";
17
+ import { resolveWorkspaceRoot, resolveWorkspaceRootWithMeta } from "./lib/workspace-root.js";
18
+ import { PROJECT_BOOTSTRAP_URI, discoverProjectResources, readProjectResourceContent, } from "./lib/project-mcp-resources.js";
19
19
  import { isAbortError, } from "./lib/tool-execution-context.js";
20
20
  const EXTENSIONS_CAPABILITY_KEY = "io.github.mybolide/extensions";
21
21
  const MAX_UI_APP_RESOURCES = 30;
@@ -424,10 +424,10 @@ const server = new Server({
424
424
  server.setRequestHandler(ListToolsRequestSchema, async () => {
425
425
  const toolset = getToolsetFromEnv();
426
426
  const filteredTools = filterTools(allToolSchemas, toolset);
427
- console.error(`[MCP Probe Kit] 当前工具集: ${toolset} (${filteredTools.length}/${allToolSchemas.length} 个工具)`);
428
- return {
429
- tools: filteredTools.map((tool) => withOutputSchema(withToolAnnotations(tool))),
430
- };
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 };
431
431
  });
432
432
  async function executeTool(name, args, context) {
433
433
  const bootstrap = ensureMcpProbeKitBootstrapForToolCall(name, args);
@@ -709,7 +709,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
709
709
  return withTraceMeta(makeToolError(errorMessage), traceMeta);
710
710
  }
711
711
  });
712
- // 定义资源列表(Cursor 设置页会展示为条目;仅保留 status + graph/latest,其余 URI 仍可 ReadResource)
712
+ // 定义资源列表(轻量只读;bootstrap 写盘仅在 resources/read probe://project/bootstrap 或工具调用时)
713
713
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
714
714
  const resources = [
715
715
  {
@@ -719,26 +719,32 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => {
719
719
  mimeType: "application/json",
720
720
  },
721
721
  {
722
- uri: "probe://graph/latest",
723
- name: "图谱快照",
724
- description: "最新图谱快照(含历史摘要与落盘索引;Markdown 可读 probe://graph/latest.md)",
722
+ uri: PROJECT_BOOTSTRAP_URI,
723
+ name: "项目 MCP 自检",
724
+ description: "读取时自动补齐 Skill + AGENTS.md,并返回 probe://project/skill|agents|context|graph 入口",
725
725
  mimeType: "application/json",
726
726
  },
727
727
  ];
728
- if (uiAppsEnabled) {
729
- for (const uri of uiAppResourceOrder.slice().reverse()) {
730
- const entry = uiAppResources.get(uri);
731
- if (!entry) {
732
- 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
+ });
733
741
  }
734
- resources.push({
735
- uri: entry.uri,
736
- name: entry.name,
737
- description: `${entry.description} (${entry.createdAt})`,
738
- mimeType: entry.mimeType,
739
- });
740
742
  }
741
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
+ }
742
748
  return {
743
749
  resources,
744
750
  };
@@ -795,11 +801,49 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
795
801
  })(),
796
802
  },
797
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
+ })(),
798
821
  }, null, 2),
799
822
  },
800
823
  ],
801
824
  };
802
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
+ }
803
847
  if (uri.startsWith("ui://")) {
804
848
  const entry = uiAppResources.get(uri);
805
849
  if (!entry) {
@@ -954,8 +998,11 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
954
998
  async function main() {
955
999
  const transport = new StdioServerTransport();
956
1000
  await server.connect(transport);
957
- const workspace = resolveWorkspaceRoot("");
958
- console.error(`MCP Probe Kit v${VERSION} 已启动 | workspace=${workspace}`);
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
+ }
959
1006
  }
960
1007
  // 启动服务器
961
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
+ });
@@ -0,0 +1,49 @@
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 { PROJECT_BOOTSTRAP_URI, ensureAndDiscoverProjectResources, readProjectResourceContent, } from "../project-mcp-resources.js";
6
+ import { MCP_PROBE_SKILL_REL_PATH } from "../workflow-skill-template.js";
7
+ function withTempProject(run) {
8
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "mcp-proj-res-"));
9
+ try {
10
+ run(root);
11
+ }
12
+ finally {
13
+ fs.rmSync(root, { recursive: true, force: true });
14
+ }
15
+ }
16
+ describe("project-mcp-resources", () => {
17
+ const prevEnv = { ...process.env };
18
+ afterEach(() => {
19
+ process.env = { ...prevEnv };
20
+ });
21
+ test("ensureAndDiscoverProjectResources auto-creates skill and agents", () => {
22
+ withTempProject((root) => {
23
+ const snapshot = ensureAndDiscoverProjectResources(root);
24
+ expect(snapshot.bootstrap.skill.created).toBe(true);
25
+ expect(snapshot.bootstrap.agentsMd.created).toBe(true);
26
+ expect(fs.existsSync(path.join(root, MCP_PROBE_SKILL_REL_PATH))).toBe(true);
27
+ expect(fs.existsSync(path.join(root, "AGENTS.md"))).toBe(true);
28
+ const skill = snapshot.resources.find((item) => item.id === "skill");
29
+ expect(skill?.exists).toBe(true);
30
+ });
31
+ });
32
+ test("readProjectResourceContent returns bootstrap catalog", () => {
33
+ withTempProject((root) => {
34
+ const content = readProjectResourceContent(PROJECT_BOOTSTRAP_URI, root);
35
+ expect(content?.mimeType).toBe("application/json");
36
+ const payload = JSON.parse(content?.text ?? "{}");
37
+ expect(payload.autoBootstrap.skill.path).toBe(MCP_PROBE_SKILL_REL_PATH);
38
+ expect(Array.isArray(payload.resources)).toBe(true);
39
+ });
40
+ });
41
+ test("readProjectResourceContent reads skill markdown", () => {
42
+ withTempProject((root) => {
43
+ ensureAndDiscoverProjectResources(root);
44
+ const content = readProjectResourceContent("probe://project/skill", root);
45
+ expect(content?.mimeType).toBe("text/markdown");
46
+ expect(content?.text).toContain("mcp-probe-kit-skill-version");
47
+ });
48
+ });
49
+ });
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { afterEach, describe, expect, test } from "vitest";
5
- import { resolveFromWorkspaceFolderPathsEnv, resolveWorkspaceRoot, } from "../workspace-root.js";
5
+ import { resolveFromWorkspaceFolderPathsEnv, resolveWorkspaceRoot, resolveWorkspaceRootWithMeta, } from "../workspace-root.js";
6
6
  const original = process.env.WORKSPACE_FOLDER_PATHS;
7
7
  const tempDirs = [];
8
8
  afterEach(() => {
@@ -28,4 +28,36 @@ describe("workspace-root WORKSPACE_FOLDER_PATHS", () => {
28
28
  expect(resolveFromWorkspaceFolderPathsEnv()).toBe(path.resolve(root));
29
29
  expect(resolveWorkspaceRoot("")).toBe(path.resolve(root));
30
30
  });
31
+ test("客户端工作区路径不向父级 layout 上爬", () => {
32
+ const parent = fs.mkdtempSync(path.join(os.tmpdir(), "ws-parent-"));
33
+ const child = path.join(parent, "my-app");
34
+ tempDirs.push(parent);
35
+ fs.mkdirSync(child, { recursive: true });
36
+ fs.writeFileSync(path.join(child, "package.json"), "{}", "utf8");
37
+ fs.mkdirSync(path.join(parent, "docs", ".mcp-probe"), { recursive: true });
38
+ fs.writeFileSync(path.join(parent, "docs", ".mcp-probe", "layout.json"), JSON.stringify({ version: 1, indexPath: "AGENTS.md", contextRoot: "docs" }), "utf8");
39
+ process.env.WORKSPACE_FOLDER_PATHS = JSON.stringify([child]);
40
+ const resolution = resolveWorkspaceRootWithMeta("");
41
+ expect(resolution.root).toBe(path.resolve(child));
42
+ expect(resolution.source).toBe("workspace-env");
43
+ });
44
+ });
45
+ describe("workspace-root explicit project_root", () => {
46
+ test("显式绝对路径优先,不走上级 layout", () => {
47
+ const parent = fs.mkdtempSync(path.join(os.tmpdir(), "ws-parent-"));
48
+ const child = path.join(parent, "my-app");
49
+ tempDirs.push(parent);
50
+ fs.mkdirSync(child, { recursive: true });
51
+ fs.mkdirSync(path.join(parent, "docs", ".mcp-probe"), { recursive: true });
52
+ fs.writeFileSync(path.join(parent, "docs", ".mcp-probe", "layout.json"), JSON.stringify({ version: 1, indexPath: "AGENTS.md", contextRoot: "docs" }), "utf8");
53
+ const resolution = resolveWorkspaceRootWithMeta(child);
54
+ expect(resolution.root).toBe(path.resolve(child));
55
+ expect(resolution.explicitHonored).toBe(true);
56
+ expect(resolution.source).toBe("explicit");
57
+ });
58
+ test("显式路径无效时返回 warning", () => {
59
+ const resolution = resolveWorkspaceRootWithMeta("not/a/valid/root");
60
+ expect(resolution.explicitHonored).toBe(false);
61
+ expect(resolution.warning).toMatch(/未能采用传入的 project_root/);
62
+ });
31
63
  });
@@ -0,0 +1,39 @@
1
+ export declare const CUSTOM_RESOURCES_MANIFEST_REL = ".mcp-probe-kit/resources.json";
2
+ export declare const CUSTOM_RESOURCE_URI_PREFIX = "project://";
3
+ export interface CustomResourceManifestEntry {
4
+ uri: string;
5
+ name: string;
6
+ description?: string;
7
+ mimeType: string;
8
+ /** 相对项目根的文件路径 */
9
+ file?: string;
10
+ /** 内联文本(与 file 二选一) */
11
+ text?: string;
12
+ /** 是否在 resources/list 中展示,默认 true */
13
+ list?: boolean;
14
+ }
15
+ export interface CustomResourcesManifestV1 {
16
+ version: 1;
17
+ resources: CustomResourceManifestEntry[];
18
+ }
19
+ export interface CustomResourceDescriptor {
20
+ uri: string;
21
+ name: string;
22
+ description: string;
23
+ mimeType: string;
24
+ }
25
+ export interface CustomResourceContent {
26
+ uri: string;
27
+ mimeType: string;
28
+ text: string;
29
+ }
30
+ export declare function resolveCustomResourcesManifestPath(projectRoot?: string): string;
31
+ export declare function parseCustomResourcesManifest(raw: string): CustomResourceManifestEntry[];
32
+ export declare function loadCustomResourcesManifest(projectRoot?: string): {
33
+ manifestPath: string | null;
34
+ entries: CustomResourceManifestEntry[];
35
+ error: string | null;
36
+ };
37
+ export declare function listCustomResourceDescriptors(projectRoot?: string): CustomResourceDescriptor[];
38
+ export declare function readCustomResourceContent(uri: string, projectRoot?: string): CustomResourceContent | null;
39
+ export declare function buildCustomResourcesManifestExample(): CustomResourcesManifestV1;