mcp-docs-service 0.3.9 → 0.4.0
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/dist/cli/bin.d.ts +8 -0
- package/dist/cli/bin.js +133 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/handlers/docs.d.ts +26 -0
- package/dist/handlers/docs.js +513 -0
- package/dist/handlers/docs.js.map +1 -0
- package/dist/handlers/documents.js +282 -0
- package/dist/handlers/file.d.ts +32 -0
- package/dist/handlers/file.js +222 -0
- package/dist/handlers/file.js.map +1 -0
- package/dist/handlers/health.js +196 -0
- package/dist/handlers/index.d.ts +1 -0
- package/dist/handlers/index.js +8 -0
- package/dist/handlers/index.js.map +1 -0
- package/dist/handlers/navigation.js +128 -0
- package/dist/index.js +108 -550
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.js +1 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/tools.d.ts +164 -0
- package/dist/schemas/tools.js +47 -0
- package/dist/schemas/tools.js.map +1 -0
- package/dist/types/docs.d.ts +74 -0
- package/dist/types/docs.js +1 -0
- package/dist/types/docs.js.map +1 -0
- package/dist/types/file.d.ts +21 -0
- package/dist/types/file.js +2 -0
- package/dist/types/file.js.map +1 -0
- package/dist/types/index.d.ts +44 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/tools.d.ts +11 -0
- package/dist/types/tools.js +1 -0
- package/dist/types/tools.js.map +1 -0
- package/dist/utils/file.d.ts +24 -0
- package/dist/utils/file.js +94 -0
- package/dist/utils/file.js.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logging.js +27 -0
- package/dist/utils/path.d.ts +16 -0
- package/dist/utils/path.js +69 -0
- package/dist/utils/path.js.map +1 -0
- package/package.json +5 -9
- package/cursor-wrapper.cjs +0 -111
- package/npx-wrapper.cjs +0 -160
package/dist/cli/bin.js
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
/**
|
3
|
+
* MCP Docs Service CLI
|
4
|
+
*
|
5
|
+
* This is the entry point for the CLI version of the MCP Docs Service.
|
6
|
+
* It simply imports and runs the main service.
|
7
|
+
*/
|
8
|
+
import path from "path";
|
9
|
+
import fs from "fs";
|
10
|
+
import { fileURLToPath } from "url";
|
11
|
+
// Get the current directory
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
13
|
+
const __dirname = path.dirname(__filename);
|
14
|
+
// Check if we're running under MCP Inspector
|
15
|
+
const isMCPInspector = process.env.MCP_INSPECTOR === "true" ||
|
16
|
+
process.argv.some((arg) => arg.includes("modelcontextprotocol/inspector"));
|
17
|
+
// Create a logging function that respects MCP Inspector mode
|
18
|
+
const log = (...args) => {
|
19
|
+
if (!isMCPInspector) {
|
20
|
+
console.log(...args);
|
21
|
+
}
|
22
|
+
};
|
23
|
+
const errorLog = (...args) => {
|
24
|
+
console.error(...args);
|
25
|
+
};
|
26
|
+
// Parse command line arguments
|
27
|
+
const args = process.argv.slice(2);
|
28
|
+
log("CLI Arguments:", JSON.stringify(args));
|
29
|
+
let docsDir = path.join(process.cwd(), "docs");
|
30
|
+
let createDir = false;
|
31
|
+
let healthCheck = false;
|
32
|
+
let showHelp = false;
|
33
|
+
// MCP Inspector specific handling
|
34
|
+
// When run through MCP Inspector, it might pass arguments in a different format
|
35
|
+
if (isMCPInspector) {
|
36
|
+
log("Detected MCP Inspector environment");
|
37
|
+
// Try to find a valid docs directory in all arguments
|
38
|
+
// This is a more aggressive approach but should work with various argument formats
|
39
|
+
for (const arg of process.argv) {
|
40
|
+
if (arg.endsWith("/docs") || arg.includes("/docs ")) {
|
41
|
+
const potentialPath = arg.split(" ")[0];
|
42
|
+
log("Found potential docs path in MCP Inspector args:", potentialPath);
|
43
|
+
if (fs.existsSync(potentialPath)) {
|
44
|
+
docsDir = path.resolve(potentialPath);
|
45
|
+
log("Using docs directory from MCP Inspector:", docsDir);
|
46
|
+
break;
|
47
|
+
}
|
48
|
+
}
|
49
|
+
}
|
50
|
+
// If we couldn't find a valid docs directory, use the default
|
51
|
+
log("Using docs directory:", docsDir);
|
52
|
+
}
|
53
|
+
else {
|
54
|
+
// Standard argument parsing
|
55
|
+
for (let i = 0; i < args.length; i++) {
|
56
|
+
log(`Processing arg[${i}]:`, args[i]);
|
57
|
+
if (args[i] === "--docs-dir" && i + 1 < args.length) {
|
58
|
+
docsDir = path.resolve(args[i + 1]);
|
59
|
+
log("Setting docs dir from --docs-dir flag:", docsDir);
|
60
|
+
i++; // Skip the next argument
|
61
|
+
}
|
62
|
+
else if (args[i] === "--create-dir") {
|
63
|
+
createDir = true;
|
64
|
+
}
|
65
|
+
else if (args[i] === "--health-check") {
|
66
|
+
healthCheck = true;
|
67
|
+
}
|
68
|
+
else if (args[i] === "--help" || args[i] === "-h") {
|
69
|
+
showHelp = true;
|
70
|
+
}
|
71
|
+
else if (!args[i].startsWith("--")) {
|
72
|
+
// Handle positional argument as docs directory
|
73
|
+
const potentialPath = path.resolve(args[i]);
|
74
|
+
log("Potential positional path:", potentialPath);
|
75
|
+
log("Path exists?", fs.existsSync(potentialPath));
|
76
|
+
if (fs.existsSync(potentialPath)) {
|
77
|
+
docsDir = potentialPath;
|
78
|
+
log("Setting docs dir from positional argument:", docsDir);
|
79
|
+
}
|
80
|
+
else {
|
81
|
+
log("Path doesn't exist, not using as docs dir:", potentialPath);
|
82
|
+
}
|
83
|
+
}
|
84
|
+
}
|
85
|
+
}
|
86
|
+
log("Final docs dir:", docsDir);
|
87
|
+
// Show help if requested
|
88
|
+
if (showHelp) {
|
89
|
+
console.log(`
|
90
|
+
MCP Docs Service - Documentation Management Service
|
91
|
+
|
92
|
+
Usage:
|
93
|
+
mcp-docs-service [options]
|
94
|
+
mcp-docs-service <docs-directory> [options]
|
95
|
+
|
96
|
+
Options:
|
97
|
+
--docs-dir <path> Specify the docs directory (default: ./docs)
|
98
|
+
--create-dir Create the docs directory if it doesn't exist
|
99
|
+
--health-check Run a health check on the documentation
|
100
|
+
--help, -h Show this help message
|
101
|
+
`);
|
102
|
+
process.exit(0);
|
103
|
+
}
|
104
|
+
// Create docs directory if it doesn't exist and --create-dir is specified
|
105
|
+
if (createDir) {
|
106
|
+
try {
|
107
|
+
if (!fs.existsSync(docsDir)) {
|
108
|
+
fs.mkdirSync(docsDir, { recursive: true });
|
109
|
+
log(`Created docs directory: ${docsDir}`);
|
110
|
+
}
|
111
|
+
}
|
112
|
+
catch (error) {
|
113
|
+
errorLog(`Error creating docs directory: ${error}`);
|
114
|
+
process.exit(1);
|
115
|
+
}
|
116
|
+
}
|
117
|
+
// Ensure the docs directory exists
|
118
|
+
if (!fs.existsSync(docsDir)) {
|
119
|
+
errorLog(`Error: Docs directory does not exist: ${docsDir}`);
|
120
|
+
errorLog(`Use --create-dir to create it automatically`);
|
121
|
+
process.exit(1);
|
122
|
+
}
|
123
|
+
// Add the docs directory to process.argv so it's available to the main service
|
124
|
+
process.argv.push(docsDir);
|
125
|
+
// Add health check flag to process.argv if specified
|
126
|
+
if (healthCheck) {
|
127
|
+
process.argv.push("--health-check");
|
128
|
+
}
|
129
|
+
// Import the main service
|
130
|
+
import "../index.js";
|
131
|
+
// The main service will handle the CLI arguments and execution
|
132
|
+
// No additional code needed here as the main index.ts already has CLI functionality
|
133
|
+
//# sourceMappingURL=bin.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../../src/cli/bin.ts"],"names":[],"mappings":"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"}
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import { ToolResponse } from "../types/tools.js";
|
2
|
+
/**
|
3
|
+
* Reads a markdown document and extracts its content and metadata
|
4
|
+
*/
|
5
|
+
export declare function readDocument(docPath: string, allowedDirectories: string[]): Promise<ToolResponse>;
|
6
|
+
/**
|
7
|
+
* Lists all markdown documents in a directory
|
8
|
+
*/
|
9
|
+
export declare function listDocuments(basePath: string, allowedDirectories: string[]): Promise<ToolResponse>;
|
10
|
+
/**
|
11
|
+
* Gets the structure of the documentation directory
|
12
|
+
*/
|
13
|
+
export declare function getStructure(basePath: string, allowedDirectories: string[]): Promise<ToolResponse>;
|
14
|
+
/**
|
15
|
+
* Gets the navigation structure for the documentation
|
16
|
+
*/
|
17
|
+
export declare function getNavigation(basePath: string, allowedDirectories: string[]): Promise<ToolResponse>;
|
18
|
+
/**
|
19
|
+
* Checks the health of documentation
|
20
|
+
*/
|
21
|
+
export declare function checkDocumentationHealth(basePath: string, options: {
|
22
|
+
checkLinks?: boolean;
|
23
|
+
checkMetadata?: boolean;
|
24
|
+
checkOrphans?: boolean;
|
25
|
+
requiredMetadataFields?: string[];
|
26
|
+
}, allowedDirectories: string[]): Promise<ToolResponse>;
|
@@ -0,0 +1,513 @@
|
|
1
|
+
import fs from "fs/promises";
|
2
|
+
import path from "path";
|
3
|
+
import { validatePath } from "../utils/path.js";
|
4
|
+
import matter from "gray-matter";
|
5
|
+
/**
|
6
|
+
* Reads a markdown document and extracts its content and metadata
|
7
|
+
*/
|
8
|
+
export async function readDocument(docPath, allowedDirectories) {
|
9
|
+
try {
|
10
|
+
const normalizedPath = await validatePath(docPath, allowedDirectories);
|
11
|
+
// Read the file
|
12
|
+
const content = await fs.readFile(normalizedPath, "utf-8");
|
13
|
+
// Parse frontmatter
|
14
|
+
const { data: metadata, content: markdownContent } = matter(content);
|
15
|
+
return {
|
16
|
+
content: [{ type: "text", text: "Document read successfully" }],
|
17
|
+
metadata: {
|
18
|
+
path: docPath,
|
19
|
+
content: markdownContent,
|
20
|
+
metadata,
|
21
|
+
},
|
22
|
+
};
|
23
|
+
}
|
24
|
+
catch (error) {
|
25
|
+
return {
|
26
|
+
content: [
|
27
|
+
{ type: "text", text: `Error reading document: ${error.message}` },
|
28
|
+
],
|
29
|
+
isError: true,
|
30
|
+
};
|
31
|
+
}
|
32
|
+
}
|
33
|
+
/**
|
34
|
+
* Lists all markdown documents in a directory
|
35
|
+
*/
|
36
|
+
export async function listDocuments(basePath, allowedDirectories) {
|
37
|
+
try {
|
38
|
+
const normalizedBasePath = basePath
|
39
|
+
? await validatePath(basePath, allowedDirectories)
|
40
|
+
: allowedDirectories[0];
|
41
|
+
const documents = [];
|
42
|
+
async function processDirectory(dirPath) {
|
43
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
44
|
+
for (const entry of entries) {
|
45
|
+
const entryPath = path.join(dirPath, entry.name);
|
46
|
+
if (entry.isDirectory()) {
|
47
|
+
await processDirectory(entryPath);
|
48
|
+
}
|
49
|
+
else if (entry.name.endsWith(".md")) {
|
50
|
+
try {
|
51
|
+
const content = await fs.readFile(entryPath, "utf-8");
|
52
|
+
const { data: metadata } = matter(content);
|
53
|
+
documents.push({
|
54
|
+
path: entryPath,
|
55
|
+
name: entry.name,
|
56
|
+
metadata: metadata,
|
57
|
+
});
|
58
|
+
}
|
59
|
+
catch (error) {
|
60
|
+
console.error(`Error processing ${entryPath}: ${error.message}`);
|
61
|
+
}
|
62
|
+
}
|
63
|
+
}
|
64
|
+
}
|
65
|
+
await processDirectory(normalizedBasePath);
|
66
|
+
return {
|
67
|
+
content: [{ type: "text", text: `Found ${documents.length} documents` }],
|
68
|
+
metadata: {
|
69
|
+
documents,
|
70
|
+
},
|
71
|
+
};
|
72
|
+
}
|
73
|
+
catch (error) {
|
74
|
+
return {
|
75
|
+
content: [
|
76
|
+
{ type: "text", text: `Error listing documents: ${error.message}` },
|
77
|
+
],
|
78
|
+
isError: true,
|
79
|
+
};
|
80
|
+
}
|
81
|
+
}
|
82
|
+
/**
|
83
|
+
* Gets the structure of the documentation directory
|
84
|
+
*/
|
85
|
+
export async function getStructure(basePath, allowedDirectories) {
|
86
|
+
try {
|
87
|
+
const normalizedBasePath = basePath
|
88
|
+
? await validatePath(basePath, allowedDirectories)
|
89
|
+
: allowedDirectories[0];
|
90
|
+
async function buildStructure(dirPath, relativePath = "") {
|
91
|
+
try {
|
92
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
93
|
+
const children = [];
|
94
|
+
let metadata;
|
95
|
+
// Check if there's an index.md file to get directory metadata
|
96
|
+
const indexPath = path.join(dirPath, "index.md");
|
97
|
+
try {
|
98
|
+
const indexStat = await fs.stat(indexPath);
|
99
|
+
if (indexStat.isFile()) {
|
100
|
+
const content = await fs.readFile(indexPath, "utf-8");
|
101
|
+
const { data } = matter(content);
|
102
|
+
metadata = data;
|
103
|
+
}
|
104
|
+
}
|
105
|
+
catch (error) {
|
106
|
+
// No index.md file, that's fine
|
107
|
+
}
|
108
|
+
// Process all entries
|
109
|
+
for (const entry of entries) {
|
110
|
+
const entryPath = path.join(dirPath, entry.name);
|
111
|
+
const entryRelativePath = path.join(relativePath, entry.name);
|
112
|
+
if (entry.isDirectory()) {
|
113
|
+
const subDir = await buildStructure(entryPath, entryRelativePath);
|
114
|
+
children.push(subDir);
|
115
|
+
}
|
116
|
+
else if (entry.name.endsWith(".md") && entry.name !== "index.md") {
|
117
|
+
try {
|
118
|
+
const content = await fs.readFile(entryPath, "utf-8");
|
119
|
+
const { data } = matter(content);
|
120
|
+
children.push({
|
121
|
+
name: entry.name,
|
122
|
+
path: entryRelativePath,
|
123
|
+
type: "file",
|
124
|
+
metadata: data,
|
125
|
+
children: [],
|
126
|
+
});
|
127
|
+
}
|
128
|
+
catch (error) {
|
129
|
+
children.push({
|
130
|
+
name: entry.name,
|
131
|
+
path: entryRelativePath,
|
132
|
+
type: "file",
|
133
|
+
error: error.message,
|
134
|
+
children: [],
|
135
|
+
});
|
136
|
+
}
|
137
|
+
}
|
138
|
+
}
|
139
|
+
// Sort children by order metadata if available, then by name
|
140
|
+
children.sort((a, b) => {
|
141
|
+
const orderA = a.metadata?.order ?? Infinity;
|
142
|
+
const orderB = b.metadata?.order ?? Infinity;
|
143
|
+
if (orderA !== orderB) {
|
144
|
+
return orderA - orderB;
|
145
|
+
}
|
146
|
+
return a.name.localeCompare(b.name);
|
147
|
+
});
|
148
|
+
return {
|
149
|
+
name: path.basename(dirPath),
|
150
|
+
path: relativePath,
|
151
|
+
type: "directory",
|
152
|
+
metadata,
|
153
|
+
children,
|
154
|
+
};
|
155
|
+
}
|
156
|
+
catch (error) {
|
157
|
+
return {
|
158
|
+
name: path.basename(dirPath),
|
159
|
+
path: relativePath,
|
160
|
+
type: "directory",
|
161
|
+
error: error.message,
|
162
|
+
children: [],
|
163
|
+
};
|
164
|
+
}
|
165
|
+
}
|
166
|
+
const structure = await buildStructure(normalizedBasePath);
|
167
|
+
return {
|
168
|
+
content: [
|
169
|
+
{
|
170
|
+
type: "text",
|
171
|
+
text: "Documentation structure retrieved successfully",
|
172
|
+
},
|
173
|
+
],
|
174
|
+
metadata: {
|
175
|
+
structure,
|
176
|
+
},
|
177
|
+
};
|
178
|
+
}
|
179
|
+
catch (error) {
|
180
|
+
return {
|
181
|
+
content: [
|
182
|
+
{ type: "text", text: `Error getting structure: ${error.message}` },
|
183
|
+
],
|
184
|
+
isError: true,
|
185
|
+
};
|
186
|
+
}
|
187
|
+
}
|
188
|
+
/**
|
189
|
+
* Gets the navigation structure for the documentation
|
190
|
+
*/
|
191
|
+
export async function getNavigation(basePath, allowedDirectories) {
|
192
|
+
try {
|
193
|
+
const normalizedBasePath = basePath
|
194
|
+
? await validatePath(basePath, allowedDirectories)
|
195
|
+
: allowedDirectories[0];
|
196
|
+
// First try to load navigation from .navigation file
|
197
|
+
const navigationFilePath = path.join(normalizedBasePath, ".navigation");
|
198
|
+
const navigationJsonPath = path.join(normalizedBasePath, "_navigation.json");
|
199
|
+
const navigationYmlPath = path.join(normalizedBasePath, "_navigation.yml");
|
200
|
+
let navigation = [];
|
201
|
+
// Try to load from .navigation file
|
202
|
+
try {
|
203
|
+
const navigationContent = await fs.readFile(navigationFilePath, "utf-8");
|
204
|
+
navigation = JSON.parse(navigationContent);
|
205
|
+
console.log("Loaded navigation from .navigation file");
|
206
|
+
return {
|
207
|
+
content: [
|
208
|
+
{
|
209
|
+
type: "text",
|
210
|
+
text: "Navigation structure loaded from .navigation file",
|
211
|
+
},
|
212
|
+
],
|
213
|
+
metadata: {
|
214
|
+
navigation,
|
215
|
+
},
|
216
|
+
};
|
217
|
+
}
|
218
|
+
catch (error) {
|
219
|
+
console.log("No .navigation file found or error loading it:", error.message);
|
220
|
+
}
|
221
|
+
// Try to load from _navigation.json file
|
222
|
+
try {
|
223
|
+
const navigationContent = await fs.readFile(navigationJsonPath, "utf-8");
|
224
|
+
navigation = JSON.parse(navigationContent);
|
225
|
+
console.log("Loaded navigation from _navigation.json file");
|
226
|
+
return {
|
227
|
+
content: [
|
228
|
+
{
|
229
|
+
type: "text",
|
230
|
+
text: "Navigation structure loaded from _navigation.json file",
|
231
|
+
},
|
232
|
+
],
|
233
|
+
metadata: {
|
234
|
+
navigation,
|
235
|
+
},
|
236
|
+
};
|
237
|
+
}
|
238
|
+
catch (error) {
|
239
|
+
console.log("No _navigation.json file found or error loading it:", error.message);
|
240
|
+
}
|
241
|
+
// If no navigation file found, build from structure
|
242
|
+
console.log("Building navigation from directory structure");
|
243
|
+
// Get the structure
|
244
|
+
const structureResponse = await getStructure(basePath, allowedDirectories);
|
245
|
+
if (structureResponse.isError) {
|
246
|
+
return structureResponse;
|
247
|
+
}
|
248
|
+
const structure = structureResponse.metadata?.structure;
|
249
|
+
// Build navigation from structure
|
250
|
+
function buildNavigation(structure) {
|
251
|
+
const sections = [];
|
252
|
+
function processNode(node, parentPath = []) {
|
253
|
+
// Skip nodes with errors
|
254
|
+
if (node.error) {
|
255
|
+
return;
|
256
|
+
}
|
257
|
+
if (node.type === "directory") {
|
258
|
+
// Create a section for this directory
|
259
|
+
const section = {
|
260
|
+
title: node.metadata?.title || node.name,
|
261
|
+
path: node.path ? `/${node.path}` : null,
|
262
|
+
items: [],
|
263
|
+
order: node.metadata?.order ?? Infinity,
|
264
|
+
};
|
265
|
+
// Process children
|
266
|
+
for (const child of node.children) {
|
267
|
+
if (child.type === "file") {
|
268
|
+
// Add file as an item
|
269
|
+
section.items.push({
|
270
|
+
title: child.metadata?.title || child.name.replace(/\.md$/, ""),
|
271
|
+
path: `/${child.path}`,
|
272
|
+
order: child.metadata?.order ?? Infinity,
|
273
|
+
});
|
274
|
+
}
|
275
|
+
else if (child.type === "directory") {
|
276
|
+
// Process subdirectory
|
277
|
+
const childSections = processNode(child, [
|
278
|
+
...parentPath,
|
279
|
+
node.name,
|
280
|
+
]);
|
281
|
+
if (childSections) {
|
282
|
+
sections.push(...childSections);
|
283
|
+
}
|
284
|
+
}
|
285
|
+
}
|
286
|
+
// Sort items by order
|
287
|
+
section.items.sort((a, b) => {
|
288
|
+
if (a.order !== b.order) {
|
289
|
+
return a.order - b.order;
|
290
|
+
}
|
291
|
+
return a.title.localeCompare(b.title);
|
292
|
+
});
|
293
|
+
// Only add section if it has items
|
294
|
+
if (section.items.length > 0) {
|
295
|
+
sections.push(section);
|
296
|
+
}
|
297
|
+
return sections;
|
298
|
+
}
|
299
|
+
return null;
|
300
|
+
}
|
301
|
+
processNode(structure);
|
302
|
+
// Sort sections by order
|
303
|
+
sections.sort((a, b) => {
|
304
|
+
if (a.order !== b.order) {
|
305
|
+
return a.order - b.order;
|
306
|
+
}
|
307
|
+
return a.title.localeCompare(b.title);
|
308
|
+
});
|
309
|
+
return sections;
|
310
|
+
}
|
311
|
+
navigation = buildNavigation(structure);
|
312
|
+
// Add debug logging
|
313
|
+
console.log("Navigation structure:", JSON.stringify(navigation, null, 2));
|
314
|
+
return {
|
315
|
+
content: [
|
316
|
+
{ type: "text", text: "Navigation structure retrieved successfully" },
|
317
|
+
],
|
318
|
+
metadata: {
|
319
|
+
navigation,
|
320
|
+
},
|
321
|
+
};
|
322
|
+
}
|
323
|
+
catch (error) {
|
324
|
+
return {
|
325
|
+
content: [
|
326
|
+
{ type: "text", text: `Error getting navigation: ${error.message}` },
|
327
|
+
],
|
328
|
+
isError: true,
|
329
|
+
};
|
330
|
+
}
|
331
|
+
}
|
332
|
+
/**
|
333
|
+
* Checks the health of documentation
|
334
|
+
*/
|
335
|
+
export async function checkDocumentationHealth(basePath, options, allowedDirectories) {
|
336
|
+
try {
|
337
|
+
// Set default options
|
338
|
+
const checkLinks = options.checkLinks !== false;
|
339
|
+
const checkMetadata = options.checkMetadata !== false;
|
340
|
+
const checkOrphans = options.checkOrphans !== false;
|
341
|
+
const requiredMetadataFields = options.requiredMetadataFields || [
|
342
|
+
"title",
|
343
|
+
"description",
|
344
|
+
"status",
|
345
|
+
];
|
346
|
+
// Use the first allowed directory if basePath is empty
|
347
|
+
const effectiveBasePath = basePath || allowedDirectories[0];
|
348
|
+
// Get all documents
|
349
|
+
const docsResult = await listDocuments(effectiveBasePath, allowedDirectories);
|
350
|
+
if (docsResult.isError) {
|
351
|
+
return docsResult;
|
352
|
+
}
|
353
|
+
const documents = docsResult.metadata?.documents || [];
|
354
|
+
// Get navigation if checking for orphans
|
355
|
+
let navigation = [];
|
356
|
+
if (checkOrphans) {
|
357
|
+
const navResult = await getNavigation(effectiveBasePath, allowedDirectories);
|
358
|
+
if (!navResult.isError && navResult.metadata?.navigation) {
|
359
|
+
navigation = navResult.metadata.navigation;
|
360
|
+
}
|
361
|
+
}
|
362
|
+
// Initialize health check result
|
363
|
+
const healthResult = {
|
364
|
+
score: 0,
|
365
|
+
totalDocuments: documents.length,
|
366
|
+
issues: [],
|
367
|
+
metadataCompleteness: 0,
|
368
|
+
brokenLinks: 0,
|
369
|
+
orphanedDocuments: 0,
|
370
|
+
missingReferences: 0,
|
371
|
+
documentsByStatus: {},
|
372
|
+
documentsByTag: {},
|
373
|
+
};
|
374
|
+
// Track documents by status and tags
|
375
|
+
documents.forEach((doc) => {
|
376
|
+
// Track by status
|
377
|
+
if (doc.metadata?.status) {
|
378
|
+
const status = doc.metadata.status;
|
379
|
+
healthResult.documentsByStatus[status] =
|
380
|
+
(healthResult.documentsByStatus[status] || 0) + 1;
|
381
|
+
}
|
382
|
+
// Track by tags
|
383
|
+
if (doc.metadata?.tags && Array.isArray(doc.metadata.tags)) {
|
384
|
+
doc.metadata.tags.forEach((tag) => {
|
385
|
+
healthResult.documentsByTag[tag] =
|
386
|
+
(healthResult.documentsByTag[tag] || 0) + 1;
|
387
|
+
});
|
388
|
+
}
|
389
|
+
});
|
390
|
+
// Check metadata completeness
|
391
|
+
if (checkMetadata) {
|
392
|
+
let totalFields = 0;
|
393
|
+
let missingFields = 0;
|
394
|
+
for (const doc of documents) {
|
395
|
+
const metadata = doc.metadata || {};
|
396
|
+
for (const field of requiredMetadataFields) {
|
397
|
+
totalFields++;
|
398
|
+
if (!metadata[field]) {
|
399
|
+
missingFields++;
|
400
|
+
healthResult.issues.push({
|
401
|
+
path: doc.path,
|
402
|
+
type: "missing_metadata",
|
403
|
+
severity: "error",
|
404
|
+
message: `Missing required metadata field: ${field}`,
|
405
|
+
details: { field },
|
406
|
+
});
|
407
|
+
}
|
408
|
+
}
|
409
|
+
}
|
410
|
+
// Calculate metadata completeness percentage
|
411
|
+
healthResult.metadataCompleteness =
|
412
|
+
totalFields > 0
|
413
|
+
? Math.round(((totalFields - missingFields) / totalFields) * 100)
|
414
|
+
: 100;
|
415
|
+
}
|
416
|
+
// Check for orphaned documents (not in navigation)
|
417
|
+
if (checkOrphans) {
|
418
|
+
// Completely disable orphaned documents check
|
419
|
+
console.log("Orphaned documents check is disabled");
|
420
|
+
healthResult.orphanedDocuments = 0;
|
421
|
+
// Ensure we don't have any orphaned document issues in the result
|
422
|
+
healthResult.issues = healthResult.issues.filter((issue) => issue.type !== "orphaned");
|
423
|
+
}
|
424
|
+
// Check for broken links
|
425
|
+
if (checkLinks) {
|
426
|
+
// Create a set of all valid document paths
|
427
|
+
const validPaths = new Set();
|
428
|
+
for (const doc of documents) {
|
429
|
+
validPaths.add(doc.path);
|
430
|
+
// Also add without .md extension
|
431
|
+
validPaths.add(doc.path.replace(/\.md$/, ""));
|
432
|
+
}
|
433
|
+
// Check each document for links
|
434
|
+
for (const doc of documents) {
|
435
|
+
try {
|
436
|
+
const content = await fs.readFile(doc.path, "utf-8");
|
437
|
+
// Find markdown links [text](link)
|
438
|
+
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
439
|
+
let match;
|
440
|
+
while ((match = linkRegex.exec(content)) !== null) {
|
441
|
+
const link = match[2];
|
442
|
+
// Only check internal links (not external URLs)
|
443
|
+
if (!link.startsWith("http://") && !link.startsWith("https://")) {
|
444
|
+
// Resolve the link relative to the document
|
445
|
+
const docDir = path.dirname(doc.path);
|
446
|
+
const resolvedPath = path.resolve(docDir, link);
|
447
|
+
// Check if the link target exists
|
448
|
+
if (!validPaths.has(resolvedPath) &&
|
449
|
+
!validPaths.has(resolvedPath + ".md")) {
|
450
|
+
healthResult.brokenLinks++;
|
451
|
+
healthResult.issues.push({
|
452
|
+
path: doc.path,
|
453
|
+
type: "broken_link",
|
454
|
+
severity: "error",
|
455
|
+
message: `Broken link: ${link}`,
|
456
|
+
details: { link, linkText: match[1] },
|
457
|
+
});
|
458
|
+
}
|
459
|
+
}
|
460
|
+
}
|
461
|
+
}
|
462
|
+
catch (error) {
|
463
|
+
// Skip files that can't be read
|
464
|
+
console.error(`Error reading file ${doc.path}:`, error);
|
465
|
+
}
|
466
|
+
}
|
467
|
+
}
|
468
|
+
// Calculate health score
|
469
|
+
// The score is based on:
|
470
|
+
// - Metadata completeness (70%)
|
471
|
+
// - No broken links (30%)
|
472
|
+
// - Orphaned documents check is disabled
|
473
|
+
const metadataScore = healthResult.metadataCompleteness * 0.7;
|
474
|
+
const brokenLinksScore = healthResult.brokenLinks === 0
|
475
|
+
? 30
|
476
|
+
: Math.max(0, 30 - (healthResult.brokenLinks / healthResult.totalDocuments) * 100);
|
477
|
+
// Calculate the final score
|
478
|
+
healthResult.score = Math.round(metadataScore + brokenLinksScore);
|
479
|
+
// Create a clean result object to ensure proper JSON formatting
|
480
|
+
const finalResult = {
|
481
|
+
score: healthResult.score,
|
482
|
+
totalDocuments: healthResult.totalDocuments,
|
483
|
+
issues: healthResult.issues,
|
484
|
+
metadataCompleteness: healthResult.metadataCompleteness,
|
485
|
+
brokenLinks: healthResult.brokenLinks,
|
486
|
+
orphanedDocuments: 0,
|
487
|
+
missingReferences: healthResult.missingReferences,
|
488
|
+
documentsByStatus: healthResult.documentsByStatus,
|
489
|
+
documentsByTag: healthResult.documentsByTag,
|
490
|
+
};
|
491
|
+
return {
|
492
|
+
content: [
|
493
|
+
{
|
494
|
+
type: "text",
|
495
|
+
text: `Documentation health check completed. Overall health score: ${finalResult.score}%`,
|
496
|
+
},
|
497
|
+
],
|
498
|
+
metadata: finalResult,
|
499
|
+
};
|
500
|
+
}
|
501
|
+
catch (error) {
|
502
|
+
return {
|
503
|
+
content: [
|
504
|
+
{
|
505
|
+
type: "text",
|
506
|
+
text: `Error checking documentation health: ${error.message}`,
|
507
|
+
},
|
508
|
+
],
|
509
|
+
isError: true,
|
510
|
+
};
|
511
|
+
}
|
512
|
+
}
|
513
|
+
//# sourceMappingURL=docs.js.map
|