mcp-probe-kit 3.6.1 → 3.6.3
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 +49 -11
- package/build/index.js +71 -24
- 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__/gitnexus-bridge.unit.test.js +1 -1
- 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.js +49 -1
- 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.js +3 -3
- package/build/lib/workflow-skill-template.js +1 -1
- package/build/lib/workspace-root.d.ts +18 -1
- package/build/lib/workspace-root.js +149 -27
- package/build/schemas/basic-tools.d.ts +1 -1
- package/build/schemas/basic-tools.js +2 -1
- package/build/schemas/index.d.ts +2 -2
- 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 +38 -7
- package/build/tools/scan_and_extract_patterns.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -11,9 +11,9 @@ function buildWorkspaceWarning(projectRoot) {
|
|
|
11
11
|
const packageRoot = path.resolve(getMcpPackageInstallRoot());
|
|
12
12
|
if (normalizedRoot === packageRoot) {
|
|
13
13
|
return [
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
14
|
+
"未能自动识别用户项目根目录,Skill/AGENTS.md 可能写入了 mcp-probe-kit 安装目录。",
|
|
15
|
+
"请从目标项目目录打开 MCP 客户端(Cursor / OpenCode 等会自动传入工作区),",
|
|
16
|
+
"或在工具参数中传 project_root 绝对路径。",
|
|
17
17
|
].join("");
|
|
18
18
|
}
|
|
19
19
|
return undefined;
|
|
@@ -37,7 +37,7 @@ export function generateWorkflowSkillBody(skillVersion = VERSION) {
|
|
|
37
37
|
return `# MCP 调用时机 — mcp-probe-kit
|
|
38
38
|
|
|
39
39
|
> 本 Skill 只回答一件事:**什么情况 → 调哪个 MCP**。不是开发流程剧本。
|
|
40
|
-
> 由 mcp-probe-kit
|
|
40
|
+
> 由 mcp-probe-kit 自动安装;支持 MCP 的 Agent 客户端可从 \`.agents/skills/\` 加载。
|
|
41
41
|
|
|
42
42
|
## 总规则
|
|
43
43
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
/** Schema / 文档:project_root 参数说明(自动探测,无需用户配置 MCP_PROJECT_ROOT) */
|
|
2
|
+
export declare const PROJECT_ROOT_SCHEMA_DESCRIPTION = "\u53EF\u9009\u3002\u9879\u76EE\u6839\u76EE\u5F55\u7EDD\u5BF9\u8DEF\u5F84\uFF1B\u672A\u4F20\u65F6\u81EA\u52A8\u4ECE MCP \u5BA2\u6237\u7AEF\u5DE5\u4F5C\u533A\u89E3\u6790\uFF08\u5982 Cursor \u6CE8\u5165 WORKSPACE_FOLDER_PATHS\u3001OpenCode/\u5BA2\u6237\u7AEF\u914D\u7F6E\u7684 cwd \u7B49\uFF09\u3002\u4EC5\u8FB9\u7F18\u573A\u666F\u9700\u624B\u52A8\u4F20\u5165\u3002";
|
|
1
3
|
/** MCP 包自身安装目录(bootstrap 误写此处说明工作区未解析成功) */
|
|
2
4
|
export declare function getMcpPackageInstallRoot(): string;
|
|
3
|
-
/** 解析
|
|
5
|
+
/** 解析 MCP 客户端注入的 WORKSPACE_FOLDER_PATHS(如 Cursor) */
|
|
4
6
|
export declare function resolveFromWorkspaceFolderPathsEnv(): string | null;
|
|
5
7
|
export declare function isLikelyProjectNamedRelativePath(inputPath?: string): boolean;
|
|
6
8
|
export declare function buildProjectRootRetryHint(inputPath?: string): {
|
|
@@ -12,5 +14,20 @@ export declare function buildProjectRootRetryHint(inputPath?: string): {
|
|
|
12
14
|
project_root: string;
|
|
13
15
|
};
|
|
14
16
|
};
|
|
17
|
+
export type WorkspaceRootSource = "explicit" | "explicit-not-yet-created" | "workspace-env" | "layout" | "env" | "cwd" | "package-fallback";
|
|
18
|
+
export interface WorkspaceRootResolution {
|
|
19
|
+
root: string;
|
|
20
|
+
source: WorkspaceRootSource;
|
|
21
|
+
/** 用户传入的 project_root 原始值 */
|
|
22
|
+
explicitRequested?: string;
|
|
23
|
+
/** 是否采用了用户显式路径(未回退到 MCP 安装目录等) */
|
|
24
|
+
explicitHonored: boolean;
|
|
25
|
+
warning?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 解析项目根目录。
|
|
29
|
+
* 若调用方显式传入 project_root(非空),优先信任该路径,不再向上游走 layout 到父目录。
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveWorkspaceRootWithMeta(explicitProjectRoot?: string): WorkspaceRootResolution;
|
|
15
32
|
export declare function resolveWorkspaceRoot(explicitProjectRoot?: string): string;
|
|
16
33
|
export declare function toWorkspacePath(explicitProjectRoot?: string): string;
|