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
|
@@ -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
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
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 { resolveFromWorkspaceFolderPathsEnv, resolveWorkspaceRoot, resolveWorkspaceRootWithMeta, } from "../workspace-root.js";
|
|
6
|
+
const original = process.env.WORKSPACE_FOLDER_PATHS;
|
|
7
|
+
const tempDirs = [];
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
if (original === undefined) {
|
|
10
|
+
delete process.env.WORKSPACE_FOLDER_PATHS;
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
process.env.WORKSPACE_FOLDER_PATHS = original;
|
|
14
|
+
}
|
|
15
|
+
while (tempDirs.length > 0) {
|
|
16
|
+
const dir = tempDirs.pop();
|
|
17
|
+
if (dir) {
|
|
18
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
describe("workspace-root WORKSPACE_FOLDER_PATHS", () => {
|
|
23
|
+
test("解析 JSON 数组形式", () => {
|
|
24
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "ws-"));
|
|
25
|
+
tempDirs.push(root);
|
|
26
|
+
fs.writeFileSync(path.join(root, "package.json"), "{}", "utf8");
|
|
27
|
+
process.env.WORKSPACE_FOLDER_PATHS = JSON.stringify([root]);
|
|
28
|
+
expect(resolveFromWorkspaceFolderPathsEnv()).toBe(path.resolve(root));
|
|
29
|
+
expect(resolveWorkspaceRoot("")).toBe(path.resolve(root));
|
|
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
|
+
});
|
|
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;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { resolveWorkspaceRoot } from "./workspace-root.js";
|
|
4
|
+
export const CUSTOM_RESOURCES_MANIFEST_REL = ".mcp-probe-kit/resources.json";
|
|
5
|
+
export const CUSTOM_RESOURCE_URI_PREFIX = "project://";
|
|
6
|
+
const DEFAULT_MAX_RESOURCES = 50;
|
|
7
|
+
const DEFAULT_MAX_FILE_BYTES = 512 * 1024;
|
|
8
|
+
function getMaxResources() {
|
|
9
|
+
const raw = process.env.MCP_CUSTOM_RESOURCES_MAX?.trim();
|
|
10
|
+
if (!raw) {
|
|
11
|
+
return DEFAULT_MAX_RESOURCES;
|
|
12
|
+
}
|
|
13
|
+
const parsed = Number.parseInt(raw, 10);
|
|
14
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_RESOURCES;
|
|
15
|
+
}
|
|
16
|
+
function getMaxFileBytes() {
|
|
17
|
+
const raw = process.env.MCP_CUSTOM_RESOURCE_MAX_BYTES?.trim();
|
|
18
|
+
if (!raw) {
|
|
19
|
+
return DEFAULT_MAX_FILE_BYTES;
|
|
20
|
+
}
|
|
21
|
+
const parsed = Number.parseInt(raw, 10);
|
|
22
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_FILE_BYTES;
|
|
23
|
+
}
|
|
24
|
+
export function resolveCustomResourcesManifestPath(projectRoot) {
|
|
25
|
+
const root = path.resolve(projectRoot ?? resolveWorkspaceRoot(""));
|
|
26
|
+
const envPath = process.env.MCP_CUSTOM_RESOURCES_PATH?.trim();
|
|
27
|
+
if (envPath) {
|
|
28
|
+
return path.isAbsolute(envPath) ? envPath : path.join(root, envPath);
|
|
29
|
+
}
|
|
30
|
+
return path.join(root, CUSTOM_RESOURCES_MANIFEST_REL);
|
|
31
|
+
}
|
|
32
|
+
function normalizeRelativeFilePath(raw) {
|
|
33
|
+
const cleaned = raw.replace(/\\/g, "/").trim().replace(/^\/+/, "");
|
|
34
|
+
if (!cleaned || cleaned === ".") {
|
|
35
|
+
throw new Error("file 不能为空");
|
|
36
|
+
}
|
|
37
|
+
if (cleaned.split("/").includes("..")) {
|
|
38
|
+
throw new Error(`file 不允许包含 '..': ${raw}`);
|
|
39
|
+
}
|
|
40
|
+
return cleaned;
|
|
41
|
+
}
|
|
42
|
+
function assertValidCustomUri(uri) {
|
|
43
|
+
if (!uri.startsWith(CUSTOM_RESOURCE_URI_PREFIX)) {
|
|
44
|
+
throw new Error(`uri 必须以 ${CUSTOM_RESOURCE_URI_PREFIX} 开头: ${uri}`);
|
|
45
|
+
}
|
|
46
|
+
const tail = uri.slice(CUSTOM_RESOURCE_URI_PREFIX.length);
|
|
47
|
+
if (!tail || !/^[a-zA-Z0-9._\-/]+$/.test(tail)) {
|
|
48
|
+
throw new Error(`uri 格式无效: ${uri}`);
|
|
49
|
+
}
|
|
50
|
+
if (uri.startsWith("probe://")) {
|
|
51
|
+
throw new Error(`uri 不可使用 probe:// 命名空间: ${uri}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function resolveFileInsideProject(projectRoot, fileRel) {
|
|
55
|
+
const normalized = normalizeRelativeFilePath(fileRel);
|
|
56
|
+
const abs = path.resolve(projectRoot, normalized);
|
|
57
|
+
const relative = path.relative(projectRoot, abs);
|
|
58
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
59
|
+
throw new Error(`file 必须位于项目根内: ${fileRel}`);
|
|
60
|
+
}
|
|
61
|
+
return abs;
|
|
62
|
+
}
|
|
63
|
+
function validateManifestEntry(entry, index, seenUris) {
|
|
64
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
65
|
+
throw new Error(`resources[${index}] 必须是对象`);
|
|
66
|
+
}
|
|
67
|
+
const record = entry;
|
|
68
|
+
const uri = typeof record.uri === "string" ? record.uri.trim() : "";
|
|
69
|
+
const name = typeof record.name === "string" ? record.name.trim() : "";
|
|
70
|
+
const mimeType = typeof record.mimeType === "string" ? record.mimeType.trim() : "";
|
|
71
|
+
const file = typeof record.file === "string" ? record.file.trim() : undefined;
|
|
72
|
+
const text = typeof record.text === "string" ? record.text : undefined;
|
|
73
|
+
const description = typeof record.description === "string" ? record.description.trim() : undefined;
|
|
74
|
+
const list = record.list === undefined ? true : Boolean(record.list);
|
|
75
|
+
if (!uri) {
|
|
76
|
+
throw new Error(`resources[${index}].uri 必填`);
|
|
77
|
+
}
|
|
78
|
+
if (!name) {
|
|
79
|
+
throw new Error(`resources[${index}].name 必填`);
|
|
80
|
+
}
|
|
81
|
+
if (!mimeType) {
|
|
82
|
+
throw new Error(`resources[${index}].mimeType 必填`);
|
|
83
|
+
}
|
|
84
|
+
assertValidCustomUri(uri);
|
|
85
|
+
if (seenUris.has(uri)) {
|
|
86
|
+
throw new Error(`resources[${index}] uri 重复: ${uri}`);
|
|
87
|
+
}
|
|
88
|
+
seenUris.add(uri);
|
|
89
|
+
const hasFile = Boolean(file);
|
|
90
|
+
const hasText = text !== undefined;
|
|
91
|
+
if (hasFile === hasText) {
|
|
92
|
+
throw new Error(`resources[${index}] 必须指定 file 或 text 之一(不可同时或皆无)`);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
uri,
|
|
96
|
+
name,
|
|
97
|
+
description,
|
|
98
|
+
mimeType,
|
|
99
|
+
...(file ? { file } : {}),
|
|
100
|
+
...(text !== undefined ? { text } : {}),
|
|
101
|
+
list,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export function parseCustomResourcesManifest(raw) {
|
|
105
|
+
let parsed;
|
|
106
|
+
try {
|
|
107
|
+
parsed = JSON.parse(raw);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
111
|
+
throw new Error(`resources.json 不是合法 JSON: ${message}`);
|
|
112
|
+
}
|
|
113
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
114
|
+
throw new Error("resources.json 根节点必须是对象");
|
|
115
|
+
}
|
|
116
|
+
const record = parsed;
|
|
117
|
+
const version = record.version;
|
|
118
|
+
if (version !== 1) {
|
|
119
|
+
throw new Error(`resources.json version 必须为 1,当前: ${String(version)}`);
|
|
120
|
+
}
|
|
121
|
+
if (!Array.isArray(record.resources)) {
|
|
122
|
+
throw new Error("resources.json 缺少 resources 数组");
|
|
123
|
+
}
|
|
124
|
+
const max = getMaxResources();
|
|
125
|
+
if (record.resources.length > max) {
|
|
126
|
+
throw new Error(`resources 数量超过上限 ${max}`);
|
|
127
|
+
}
|
|
128
|
+
const seenUris = new Set();
|
|
129
|
+
return record.resources.map((entry, index) => validateManifestEntry(entry, index, seenUris));
|
|
130
|
+
}
|
|
131
|
+
export function loadCustomResourcesManifest(projectRoot) {
|
|
132
|
+
const manifestPath = resolveCustomResourcesManifestPath(projectRoot);
|
|
133
|
+
if (!fs.existsSync(manifestPath)) {
|
|
134
|
+
return { manifestPath: null, entries: [], error: null };
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const raw = fs.readFileSync(manifestPath, "utf-8");
|
|
138
|
+
const entries = parseCustomResourcesManifest(raw);
|
|
139
|
+
return { manifestPath, entries, error: null };
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
143
|
+
console.error(`[MCP Probe Kit] 自定义 resources 清单加载失败: ${message}`);
|
|
144
|
+
return { manifestPath, entries: [], error: message };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export function listCustomResourceDescriptors(projectRoot) {
|
|
148
|
+
const { entries } = loadCustomResourcesManifest(projectRoot);
|
|
149
|
+
return entries
|
|
150
|
+
.filter((entry) => entry.list !== false)
|
|
151
|
+
.map((entry) => ({
|
|
152
|
+
uri: entry.uri,
|
|
153
|
+
name: entry.name,
|
|
154
|
+
description: entry.description ?? `项目自建资源 (${entry.uri})`,
|
|
155
|
+
mimeType: entry.mimeType,
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
export function readCustomResourceContent(uri, projectRoot) {
|
|
159
|
+
if (!uri.startsWith(CUSTOM_RESOURCE_URI_PREFIX)) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
const root = path.resolve(projectRoot ?? resolveWorkspaceRoot(""));
|
|
163
|
+
const { entries, error } = loadCustomResourcesManifest(root);
|
|
164
|
+
const entry = entries.find((item) => item.uri === uri);
|
|
165
|
+
if (!entry) {
|
|
166
|
+
if (error) {
|
|
167
|
+
throw new Error(`自定义 resources 清单无效,无法读取 ${uri}: ${error}`);
|
|
168
|
+
}
|
|
169
|
+
throw new Error(`未找到自定义 resource: ${uri}`);
|
|
170
|
+
}
|
|
171
|
+
if (entry.text !== undefined) {
|
|
172
|
+
return {
|
|
173
|
+
uri: entry.uri,
|
|
174
|
+
mimeType: entry.mimeType,
|
|
175
|
+
text: entry.text,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (!entry.file) {
|
|
179
|
+
throw new Error(`resource 配置无效(缺少 file/text): ${uri}`);
|
|
180
|
+
}
|
|
181
|
+
const absPath = resolveFileInsideProject(root, entry.file);
|
|
182
|
+
if (!fs.existsSync(absPath)) {
|
|
183
|
+
throw new Error(`resource 文件不存在: ${entry.file}`);
|
|
184
|
+
}
|
|
185
|
+
const stat = fs.statSync(absPath);
|
|
186
|
+
if (!stat.isFile()) {
|
|
187
|
+
throw new Error(`resource 路径不是文件: ${entry.file}`);
|
|
188
|
+
}
|
|
189
|
+
const maxBytes = getMaxFileBytes();
|
|
190
|
+
if (stat.size > maxBytes) {
|
|
191
|
+
throw new Error(`resource 文件过大 (${stat.size} > ${maxBytes} bytes): ${entry.file}`);
|
|
192
|
+
}
|
|
193
|
+
const text = fs.readFileSync(absPath, "utf-8");
|
|
194
|
+
return {
|
|
195
|
+
uri: entry.uri,
|
|
196
|
+
mimeType: entry.mimeType,
|
|
197
|
+
text,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
export function buildCustomResourcesManifestExample() {
|
|
201
|
+
return {
|
|
202
|
+
version: 1,
|
|
203
|
+
resources: [
|
|
204
|
+
{
|
|
205
|
+
uri: "project://docs/architecture",
|
|
206
|
+
name: "架构说明",
|
|
207
|
+
description: "项目架构文档(docs/architecture.md)",
|
|
208
|
+
mimeType: "text/markdown",
|
|
209
|
+
file: "docs/architecture.md",
|
|
210
|
+
list: true,
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
uri: "project://team/oncall",
|
|
214
|
+
name: "值班信息",
|
|
215
|
+
description: "内联文本示例",
|
|
216
|
+
mimeType: "text/plain",
|
|
217
|
+
text: "Oncall: team@example.com",
|
|
218
|
+
list: false,
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
@@ -3,9 +3,17 @@
|
|
|
3
3
|
*/
|
|
4
4
|
type JsonSchema = Record<string, unknown>;
|
|
5
5
|
export declare function getOutputSchemaForTool(toolName: string): JsonSchema | undefined;
|
|
6
|
+
/** Cursor 等对 tools/list 体积敏感时,默认不在列表里附带 outputSchema(调用仍返回 structuredContent) */
|
|
7
|
+
export declare function shouldIncludeOutputSchemaInToolsList(): boolean;
|
|
6
8
|
export declare function withOutputSchema<T extends {
|
|
7
9
|
name: string;
|
|
8
10
|
}>(tool: T): T & {
|
|
9
11
|
outputSchema?: JsonSchema;
|
|
10
12
|
};
|
|
13
|
+
/** tools/list 返回前的最终形态(默认省略 outputSchema 以兼容 Cursor lease) */
|
|
14
|
+
export declare function prepareToolForToolsList<T extends {
|
|
15
|
+
name: string;
|
|
16
|
+
}>(tool: T): T & {
|
|
17
|
+
annotations?: import("./tool-annotations.js").ToolAnnotations;
|
|
18
|
+
};
|
|
11
19
|
export {};
|
|
@@ -8,6 +8,7 @@ import { InterviewReportSchema } from '../schemas/output/product-design-tools.js
|
|
|
8
8
|
import { CommitGuidanceSchema, FeatureReportSchema, BugFixReportSchema, UIReportSchema, OnboardingReportSchema, RalphLoopReportSchema, WorkflowReportSchema, } from '../schemas/structured-output.js';
|
|
9
9
|
import { MemorySearchSchema, MemoryAssetDetailSchema, MemorizeResultSchema, DeleteMemoryResultSchema, UpdateMemoryResultSchema, PatternExtractionSchema, } from '../schemas/output/memory-tools.js';
|
|
10
10
|
import { CodeInsightSchema } from '../schemas/output/code-insight-tools.js';
|
|
11
|
+
import { withToolAnnotations } from './tool-annotations.js';
|
|
11
12
|
const SpecValidationReportSchema = {
|
|
12
13
|
type: 'object',
|
|
13
14
|
properties: {
|
|
@@ -74,7 +75,20 @@ const OUTPUT_SCHEMA_BY_TOOL = {
|
|
|
74
75
|
export function getOutputSchemaForTool(toolName) {
|
|
75
76
|
return OUTPUT_SCHEMA_BY_TOOL[toolName];
|
|
76
77
|
}
|
|
78
|
+
/** Cursor 等对 tools/list 体积敏感时,默认不在列表里附带 outputSchema(调用仍返回 structuredContent) */
|
|
79
|
+
export function shouldIncludeOutputSchemaInToolsList() {
|
|
80
|
+
const raw = process.env.MCP_INCLUDE_OUTPUT_SCHEMA;
|
|
81
|
+
if (raw === undefined) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return /^(1|true|yes|on)$/i.test(raw.trim());
|
|
85
|
+
}
|
|
77
86
|
export function withOutputSchema(tool) {
|
|
78
87
|
const outputSchema = getOutputSchemaForTool(tool.name);
|
|
79
88
|
return outputSchema ? { ...tool, outputSchema } : tool;
|
|
80
89
|
}
|
|
90
|
+
/** tools/list 返回前的最终形态(默认省略 outputSchema 以兼容 Cursor lease) */
|
|
91
|
+
export function prepareToolForToolsList(tool) {
|
|
92
|
+
const annotated = withToolAnnotations(tool);
|
|
93
|
+
return shouldIncludeOutputSchemaInToolsList() ? withOutputSchema(annotated) : annotated;
|
|
94
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type McpProbeKitBootstrapResult } from "./workflow-skill-installer.js";
|
|
2
|
+
export declare const PROJECT_BOOTSTRAP_URI = "probe://project/bootstrap";
|
|
3
|
+
export declare const PROJECT_RESOURCE_PREFIX = "probe://project/";
|
|
4
|
+
export type ProjectResourceId = "skill" | "agents" | "context" | "graph";
|
|
5
|
+
export interface ProjectResourceEntry {
|
|
6
|
+
id: ProjectResourceId;
|
|
7
|
+
uri: string;
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
mimeType: string;
|
|
11
|
+
fileRel: string;
|
|
12
|
+
exists: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface ProjectResourcesSnapshot {
|
|
15
|
+
projectRoot: string;
|
|
16
|
+
bootstrap: McpProbeKitBootstrapResult;
|
|
17
|
+
resources: ProjectResourceEntry[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 只读发现项目资源(不写盘)。用于 resources/list、probe://status 等热路径。
|
|
21
|
+
*/
|
|
22
|
+
export declare function discoverProjectResources(projectRoot?: string): Omit<ProjectResourcesSnapshot, "bootstrap"> & {
|
|
23
|
+
bootstrap?: never;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* 自检并补齐 Skill + AGENTS.md,再按约定路径发现可读 Resource(按需调用,勿在 resources/list 中调用)。
|
|
27
|
+
*/
|
|
28
|
+
export declare function ensureAndDiscoverProjectResources(projectRoot?: string): ProjectResourcesSnapshot;
|
|
29
|
+
export declare function buildProjectBootstrapPayload(snapshot: ProjectResourcesSnapshot): {
|
|
30
|
+
projectRoot: string;
|
|
31
|
+
autoBootstrap: {
|
|
32
|
+
skill: {
|
|
33
|
+
path: string;
|
|
34
|
+
created: boolean;
|
|
35
|
+
updated: boolean;
|
|
36
|
+
version: string;
|
|
37
|
+
};
|
|
38
|
+
agentsMd: {
|
|
39
|
+
path: string;
|
|
40
|
+
created: boolean;
|
|
41
|
+
updated: boolean;
|
|
42
|
+
};
|
|
43
|
+
workspaceWarning: string | null;
|
|
44
|
+
};
|
|
45
|
+
resources: {
|
|
46
|
+
id: ProjectResourceId;
|
|
47
|
+
uri: string;
|
|
48
|
+
name: string;
|
|
49
|
+
description: string;
|
|
50
|
+
mimeType: string;
|
|
51
|
+
file: string;
|
|
52
|
+
exists: boolean;
|
|
53
|
+
}[];
|
|
54
|
+
readHint: string;
|
|
55
|
+
};
|
|
56
|
+
export declare function resolveProjectResourceId(uri: string): ProjectResourceId | null;
|
|
57
|
+
export declare function readProjectResourceContent(uri: string, projectRoot?: string): {
|
|
58
|
+
uri: string;
|
|
59
|
+
mimeType: string;
|
|
60
|
+
text: string;
|
|
61
|
+
} | null;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { ensureMcpProbeKitBootstrap, } from "./workflow-skill-installer.js";
|
|
4
|
+
import { MCP_PROBE_SKILL_REL_PATH } from "./workflow-skill-template.js";
|
|
5
|
+
import { resolveProjectContextLayout, toPosixPath } from "./project-context-layout.js";
|
|
6
|
+
import { resolveWorkspaceRoot } from "./workspace-root.js";
|
|
7
|
+
export const PROJECT_BOOTSTRAP_URI = "probe://project/bootstrap";
|
|
8
|
+
export const PROJECT_RESOURCE_PREFIX = "probe://project/";
|
|
9
|
+
const DEFAULT_MAX_FILE_BYTES = 512 * 1024;
|
|
10
|
+
function getMaxFileBytes() {
|
|
11
|
+
const raw = process.env.MCP_PROJECT_RESOURCE_MAX_BYTES?.trim();
|
|
12
|
+
if (!raw) {
|
|
13
|
+
return DEFAULT_MAX_FILE_BYTES;
|
|
14
|
+
}
|
|
15
|
+
const parsed = Number.parseInt(raw, 10);
|
|
16
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_FILE_BYTES;
|
|
17
|
+
}
|
|
18
|
+
function fileExists(projectRoot, rel) {
|
|
19
|
+
try {
|
|
20
|
+
return fs.existsSync(path.join(projectRoot, rel)) && fs.statSync(path.join(projectRoot, rel)).isFile();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function buildResourceEntries(projectRoot, layout) {
|
|
27
|
+
const skillRel = MCP_PROBE_SKILL_REL_PATH;
|
|
28
|
+
const agentsRel = toPosixPath(layout.indexPath);
|
|
29
|
+
const contextRel = layout.indexStyle === "legacy"
|
|
30
|
+
? toPosixPath(layout.legacyIndexPath)
|
|
31
|
+
: agentsRel;
|
|
32
|
+
const graphRel = toPosixPath(layout.latestMarkdownPath);
|
|
33
|
+
const defs = [
|
|
34
|
+
{
|
|
35
|
+
id: "skill",
|
|
36
|
+
uri: `${PROJECT_RESOURCE_PREFIX}skill`,
|
|
37
|
+
name: "MCP 调用时机 Skill",
|
|
38
|
+
description: "自动维护的 .agents/skills/mcp-probe-kit/SKILL.md",
|
|
39
|
+
mimeType: "text/markdown",
|
|
40
|
+
fileRel: skillRel,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "agents",
|
|
44
|
+
uri: `${PROJECT_RESOURCE_PREFIX}agents`,
|
|
45
|
+
name: "AGENTS.md",
|
|
46
|
+
description: "项目 Agent 规则与 mcp-probe 上下文块",
|
|
47
|
+
mimeType: "text/markdown",
|
|
48
|
+
fileRel: agentsRel,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "context",
|
|
52
|
+
uri: `${PROJECT_RESOURCE_PREFIX}context`,
|
|
53
|
+
name: "项目上下文索引",
|
|
54
|
+
description: "AGENTS.md 或 docs/project-context.md 入口",
|
|
55
|
+
mimeType: "text/markdown",
|
|
56
|
+
fileRel: contextRel,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "graph",
|
|
60
|
+
uri: `${PROJECT_RESOURCE_PREFIX}graph`,
|
|
61
|
+
name: "图谱洞察(最新)",
|
|
62
|
+
description: "docs/graph-insights/latest.md(存在时)",
|
|
63
|
+
mimeType: "text/markdown",
|
|
64
|
+
fileRel: graphRel,
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
return defs.map((item) => ({
|
|
68
|
+
...item,
|
|
69
|
+
exists: fileExists(projectRoot, item.fileRel),
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 只读发现项目资源(不写盘)。用于 resources/list、probe://status 等热路径。
|
|
74
|
+
*/
|
|
75
|
+
export function discoverProjectResources(projectRoot) {
|
|
76
|
+
const root = path.resolve(projectRoot ?? resolveWorkspaceRoot(""));
|
|
77
|
+
const layout = resolveProjectContextLayout(root);
|
|
78
|
+
const resources = buildResourceEntries(root, layout);
|
|
79
|
+
return {
|
|
80
|
+
projectRoot: root,
|
|
81
|
+
resources,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 自检并补齐 Skill + AGENTS.md,再按约定路径发现可读 Resource(按需调用,勿在 resources/list 中调用)。
|
|
86
|
+
*/
|
|
87
|
+
export function ensureAndDiscoverProjectResources(projectRoot) {
|
|
88
|
+
const root = path.resolve(projectRoot ?? resolveWorkspaceRoot(""));
|
|
89
|
+
const bootstrap = ensureMcpProbeKitBootstrap(root);
|
|
90
|
+
const layout = resolveProjectContextLayout(root);
|
|
91
|
+
const resources = buildResourceEntries(root, layout);
|
|
92
|
+
return {
|
|
93
|
+
projectRoot: root,
|
|
94
|
+
bootstrap,
|
|
95
|
+
resources,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export function buildProjectBootstrapPayload(snapshot) {
|
|
99
|
+
const { bootstrap, resources } = snapshot;
|
|
100
|
+
return {
|
|
101
|
+
projectRoot: toPosixPath(snapshot.projectRoot),
|
|
102
|
+
autoBootstrap: {
|
|
103
|
+
skill: {
|
|
104
|
+
path: bootstrap.skill.skillRelPath,
|
|
105
|
+
created: bootstrap.skill.created,
|
|
106
|
+
updated: bootstrap.skill.updated,
|
|
107
|
+
version: bootstrap.skill.version,
|
|
108
|
+
},
|
|
109
|
+
agentsMd: {
|
|
110
|
+
path: toPosixPath(bootstrap.agentsMd.path),
|
|
111
|
+
created: bootstrap.agentsMd.created,
|
|
112
|
+
updated: bootstrap.agentsMd.updated,
|
|
113
|
+
},
|
|
114
|
+
workspaceWarning: bootstrap.workspaceWarning ?? null,
|
|
115
|
+
},
|
|
116
|
+
resources: resources.map((item) => ({
|
|
117
|
+
id: item.id,
|
|
118
|
+
uri: item.uri,
|
|
119
|
+
name: item.name,
|
|
120
|
+
description: item.description,
|
|
121
|
+
mimeType: item.mimeType,
|
|
122
|
+
file: item.fileRel,
|
|
123
|
+
exists: item.exists,
|
|
124
|
+
})),
|
|
125
|
+
readHint: "无需配置文件。resources/read 按需读取 probe://project/skill、agents、context、graph;缺失项会在 tools/list 或工具调用时自动补齐。",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export function resolveProjectResourceId(uri) {
|
|
129
|
+
if (!uri.startsWith(PROJECT_RESOURCE_PREFIX)) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const tail = uri.slice(PROJECT_RESOURCE_PREFIX.length);
|
|
133
|
+
if (tail === "bootstrap") {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
if (tail === "skill" || tail === "agents" || tail === "context" || tail === "graph") {
|
|
137
|
+
return tail;
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
export function readProjectResourceContent(uri, projectRoot) {
|
|
142
|
+
if (uri === PROJECT_BOOTSTRAP_URI) {
|
|
143
|
+
const snapshot = ensureAndDiscoverProjectResources(projectRoot);
|
|
144
|
+
return {
|
|
145
|
+
uri,
|
|
146
|
+
mimeType: "application/json",
|
|
147
|
+
text: JSON.stringify(buildProjectBootstrapPayload(snapshot), null, 2),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const id = resolveProjectResourceId(uri);
|
|
151
|
+
if (!id) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const snapshot = ensureAndDiscoverProjectResources(projectRoot);
|
|
155
|
+
const entry = snapshot.resources.find((item) => item.id === id);
|
|
156
|
+
if (!entry) {
|
|
157
|
+
throw new Error(`未知项目 resource: ${uri}`);
|
|
158
|
+
}
|
|
159
|
+
if (!entry.exists) {
|
|
160
|
+
throw new Error(`项目 resource 文件尚不存在: ${entry.fileRel}(可先调用 init_project_context 或任意 MCP 工具触发自动补齐)`);
|
|
161
|
+
}
|
|
162
|
+
const absPath = path.join(snapshot.projectRoot, entry.fileRel);
|
|
163
|
+
const stat = fs.statSync(absPath);
|
|
164
|
+
const maxBytes = getMaxFileBytes();
|
|
165
|
+
if (stat.size > maxBytes) {
|
|
166
|
+
throw new Error(`文件过大 (${stat.size} > ${maxBytes} bytes): ${entry.fileRel}`);
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
uri: entry.uri,
|
|
170
|
+
mimeType: entry.mimeType,
|
|
171
|
+
text: fs.readFileSync(absPath, "utf-8"),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -17,6 +17,8 @@ export interface McpProbeKitBootstrapResult {
|
|
|
17
17
|
projectRoot: string;
|
|
18
18
|
skill: SkillEnsureResult;
|
|
19
19
|
agentsMd: AgentsMdEnsureResult;
|
|
20
|
+
/** 工作区可能解析失败(写到了 mcp-probe-kit 安装目录) */
|
|
21
|
+
workspaceWarning?: string;
|
|
20
22
|
}
|
|
21
23
|
export declare function resolveProjectRootFromToolArgs(args: unknown): string;
|
|
22
24
|
/**
|