mcp-docs-service 0.3.10 → 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 +107 -549
- 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 +4 -8
- package/cursor-wrapper.cjs +0 -111
- package/npx-wrapper.cjs +0 -160
@@ -0,0 +1,196 @@
|
|
1
|
+
/**
|
2
|
+
* Health check handlers for the MCP Docs Service
|
3
|
+
*
|
4
|
+
* These handlers implement the documentation health check functionality.
|
5
|
+
*/
|
6
|
+
import fs from "fs/promises";
|
7
|
+
import path from "path";
|
8
|
+
import { glob } from "glob";
|
9
|
+
import { parseFrontmatter } from "./documents.js";
|
10
|
+
import { NavigationHandler } from "./navigation.js";
|
11
|
+
export class HealthCheckHandler {
|
12
|
+
docsDir;
|
13
|
+
navigationHandler;
|
14
|
+
constructor(docsDir) {
|
15
|
+
this.docsDir = docsDir;
|
16
|
+
this.navigationHandler = new NavigationHandler(docsDir);
|
17
|
+
}
|
18
|
+
/**
|
19
|
+
* Check documentation health
|
20
|
+
*/
|
21
|
+
async checkDocumentationHealth(basePath = "") {
|
22
|
+
try {
|
23
|
+
const baseDir = path.join(this.docsDir, basePath);
|
24
|
+
const pattern = path.join(baseDir, "**/*.md");
|
25
|
+
const files = await glob(pattern);
|
26
|
+
const results = {
|
27
|
+
score: 0,
|
28
|
+
totalDocuments: files.length,
|
29
|
+
issues: [],
|
30
|
+
metadataCompleteness: 0,
|
31
|
+
brokenLinks: 0,
|
32
|
+
orphanedDocuments: 0,
|
33
|
+
missingReferences: 0,
|
34
|
+
documentsByStatus: {},
|
35
|
+
documentsByTag: {},
|
36
|
+
};
|
37
|
+
// Check frontmatter and content
|
38
|
+
let totalMetadataFields = 0;
|
39
|
+
let presentMetadataFields = 0;
|
40
|
+
for (const file of files) {
|
41
|
+
const relativePath = path.relative(this.docsDir, file);
|
42
|
+
const content = await fs.readFile(file, "utf-8");
|
43
|
+
const { frontmatter } = parseFrontmatter(content);
|
44
|
+
// Check for required metadata
|
45
|
+
const requiredFields = ["title", "description"];
|
46
|
+
totalMetadataFields += requiredFields.length;
|
47
|
+
if (Object.keys(frontmatter).length === 0) {
|
48
|
+
results.issues.push({
|
49
|
+
path: relativePath,
|
50
|
+
type: "missing_metadata",
|
51
|
+
severity: "error",
|
52
|
+
message: "Missing frontmatter",
|
53
|
+
});
|
54
|
+
}
|
55
|
+
for (const field of requiredFields) {
|
56
|
+
if (!frontmatter[field]) {
|
57
|
+
results.issues.push({
|
58
|
+
path: relativePath,
|
59
|
+
type: "missing_metadata",
|
60
|
+
severity: "warning",
|
61
|
+
message: `Missing ${field} in frontmatter`,
|
62
|
+
});
|
63
|
+
}
|
64
|
+
else {
|
65
|
+
presentMetadataFields++;
|
66
|
+
}
|
67
|
+
}
|
68
|
+
// Track documents by status
|
69
|
+
if (frontmatter.status) {
|
70
|
+
results.documentsByStatus[frontmatter.status] =
|
71
|
+
(results.documentsByStatus[frontmatter.status] || 0) + 1;
|
72
|
+
}
|
73
|
+
// Track documents by tags
|
74
|
+
if (frontmatter.tags && Array.isArray(frontmatter.tags)) {
|
75
|
+
for (const tag of frontmatter.tags) {
|
76
|
+
results.documentsByTag[tag] =
|
77
|
+
(results.documentsByTag[tag] || 0) + 1;
|
78
|
+
}
|
79
|
+
}
|
80
|
+
// Check for internal links
|
81
|
+
const linkRegex = /\[.*?\]\((.*?)\)/g;
|
82
|
+
let match;
|
83
|
+
while ((match = linkRegex.exec(content)) !== null) {
|
84
|
+
const link = match[1];
|
85
|
+
// Only check relative links to markdown files
|
86
|
+
if (!link.startsWith("http") &&
|
87
|
+
!link.startsWith("#") &&
|
88
|
+
link.endsWith(".md")) {
|
89
|
+
const linkPath = path.join(path.dirname(file), link);
|
90
|
+
try {
|
91
|
+
await fs.access(linkPath);
|
92
|
+
}
|
93
|
+
catch {
|
94
|
+
results.brokenLinks++;
|
95
|
+
results.issues.push({
|
96
|
+
path: relativePath,
|
97
|
+
type: "broken_link",
|
98
|
+
severity: "error",
|
99
|
+
message: `Broken link to ${link}`,
|
100
|
+
});
|
101
|
+
}
|
102
|
+
}
|
103
|
+
}
|
104
|
+
}
|
105
|
+
// Calculate metadata completeness percentage
|
106
|
+
results.metadataCompleteness =
|
107
|
+
totalMetadataFields > 0
|
108
|
+
? Math.round((presentMetadataFields / totalMetadataFields) * 100)
|
109
|
+
: 100;
|
110
|
+
// Generate navigation to check for orphaned documents
|
111
|
+
const navResponse = await this.navigationHandler.generateNavigation(basePath);
|
112
|
+
if (!navResponse.isError && navResponse.content[0].text) {
|
113
|
+
const navigation = JSON.parse(navResponse.content[0].text);
|
114
|
+
function collectPaths(items) {
|
115
|
+
let paths = [];
|
116
|
+
for (const item of items) {
|
117
|
+
if (item.path) {
|
118
|
+
paths.push(item.path);
|
119
|
+
}
|
120
|
+
if (item.children && item.children.length > 0) {
|
121
|
+
paths = paths.concat(collectPaths(item.children));
|
122
|
+
}
|
123
|
+
}
|
124
|
+
return paths;
|
125
|
+
}
|
126
|
+
const navigationPaths = collectPaths(navigation);
|
127
|
+
for (const file of files) {
|
128
|
+
const relativePath = path.relative(this.docsDir, file);
|
129
|
+
if (!navigationPaths.includes(relativePath)) {
|
130
|
+
results.orphanedDocuments++;
|
131
|
+
results.issues.push({
|
132
|
+
path: relativePath,
|
133
|
+
type: "orphaned",
|
134
|
+
severity: "warning",
|
135
|
+
message: "Orphaned document (not in navigation)",
|
136
|
+
});
|
137
|
+
}
|
138
|
+
}
|
139
|
+
}
|
140
|
+
// Calculate health score (0-100)
|
141
|
+
const issueWeights = {
|
142
|
+
missing_metadata: 1,
|
143
|
+
broken_link: 2,
|
144
|
+
orphaned: 1,
|
145
|
+
missing_reference: 1,
|
146
|
+
};
|
147
|
+
let weightedIssueCount = 0;
|
148
|
+
for (const issue of results.issues) {
|
149
|
+
weightedIssueCount += issueWeights[issue.type] || 1;
|
150
|
+
}
|
151
|
+
const maxIssues = results.totalDocuments * 5; // 5 possible issues per document
|
152
|
+
results.score = Math.max(0, 100 - Math.round((weightedIssueCount / maxIssues) * 100));
|
153
|
+
// Format the response
|
154
|
+
const formattedResponse = `Documentation Health Report:
|
155
|
+
Health Score: ${results.score}/100
|
156
|
+
|
157
|
+
Summary:
|
158
|
+
- Total Documents: ${results.totalDocuments}
|
159
|
+
- Metadata Completeness: ${results.metadataCompleteness}%
|
160
|
+
- Broken Links: ${results.brokenLinks}
|
161
|
+
- Orphaned Documents: ${results.orphanedDocuments}
|
162
|
+
|
163
|
+
Issues:
|
164
|
+
${results.issues
|
165
|
+
.map((issue) => `- ${issue.path}: ${issue.message} (${issue.severity})`)
|
166
|
+
.join("\n")}
|
167
|
+
|
168
|
+
Document Status:
|
169
|
+
${Object.entries(results.documentsByStatus)
|
170
|
+
.map(([status, count]) => `- ${status}: ${count}`)
|
171
|
+
.join("\n") || "- No status information available"}
|
172
|
+
|
173
|
+
Document Tags:
|
174
|
+
${Object.entries(results.documentsByTag)
|
175
|
+
.map(([tag, count]) => `- ${tag}: ${count}`)
|
176
|
+
.join("\n") || "- No tag information available"}
|
177
|
+
`;
|
178
|
+
return {
|
179
|
+
content: [{ type: "text", text: formattedResponse }],
|
180
|
+
metadata: results,
|
181
|
+
};
|
182
|
+
}
|
183
|
+
catch (error) {
|
184
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
185
|
+
return {
|
186
|
+
content: [
|
187
|
+
{
|
188
|
+
type: "text",
|
189
|
+
text: `Error checking documentation health: ${errorMessage}`,
|
190
|
+
},
|
191
|
+
],
|
192
|
+
isError: true,
|
193
|
+
};
|
194
|
+
}
|
195
|
+
}
|
196
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from "./docs.js";
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/handlers/index.ts"],"names":[],"mappings":"AAAA,yBAAyB;AACzB,cAAc,WAAW,CAAC"}
|
@@ -0,0 +1,128 @@
|
|
1
|
+
/**
|
2
|
+
* Navigation handlers for the MCP Docs Service
|
3
|
+
*
|
4
|
+
* These handlers implement the navigation structure generation.
|
5
|
+
*/
|
6
|
+
import fs from "fs/promises";
|
7
|
+
import path from "path";
|
8
|
+
import { glob } from "glob";
|
9
|
+
import { parseFrontmatter } from "./documents.js";
|
10
|
+
export class NavigationHandler {
|
11
|
+
docsDir;
|
12
|
+
constructor(docsDir) {
|
13
|
+
this.docsDir = docsDir;
|
14
|
+
}
|
15
|
+
/**
|
16
|
+
* Generate navigation structure from documents
|
17
|
+
*/
|
18
|
+
async generateNavigation(basePath = "") {
|
19
|
+
try {
|
20
|
+
const baseDir = path.join(this.docsDir, basePath);
|
21
|
+
const pattern = path.join(baseDir, "**/*.md");
|
22
|
+
const files = await glob(pattern);
|
23
|
+
// Sort files to ensure consistent order and process index.md files first
|
24
|
+
files.sort((a, b) => {
|
25
|
+
const aIsIndex = path.basename(a) === "index.md";
|
26
|
+
const bIsIndex = path.basename(b) === "index.md";
|
27
|
+
if (aIsIndex && !bIsIndex)
|
28
|
+
return -1;
|
29
|
+
if (!aIsIndex && bIsIndex)
|
30
|
+
return 1;
|
31
|
+
return a.localeCompare(b);
|
32
|
+
});
|
33
|
+
const navigation = [];
|
34
|
+
const directoryMap = {};
|
35
|
+
for (const file of files) {
|
36
|
+
const relativePath = path.relative(this.docsDir, file);
|
37
|
+
const content = await fs.readFile(file, "utf-8");
|
38
|
+
const { frontmatter } = parseFrontmatter(content);
|
39
|
+
const title = frontmatter.title || path.basename(file, ".md");
|
40
|
+
const order = frontmatter.order !== undefined ? Number(frontmatter.order) : 999;
|
41
|
+
const item = {
|
42
|
+
title,
|
43
|
+
path: relativePath,
|
44
|
+
order,
|
45
|
+
children: [],
|
46
|
+
};
|
47
|
+
const dirPath = path.dirname(relativePath);
|
48
|
+
if (dirPath === "." || dirPath === basePath) {
|
49
|
+
navigation.push(item);
|
50
|
+
}
|
51
|
+
else {
|
52
|
+
// Create parent directories if they don't exist in the navigation
|
53
|
+
const pathParts = dirPath.split(path.sep);
|
54
|
+
let currentPath = "";
|
55
|
+
let currentNavigation = navigation;
|
56
|
+
for (const part of pathParts) {
|
57
|
+
currentPath = currentPath ? path.join(currentPath, part) : part;
|
58
|
+
if (!directoryMap[currentPath]) {
|
59
|
+
const dirItem = {
|
60
|
+
title: part,
|
61
|
+
path: currentPath,
|
62
|
+
order: 0,
|
63
|
+
children: [],
|
64
|
+
};
|
65
|
+
directoryMap[currentPath] = dirItem;
|
66
|
+
currentNavigation.push(dirItem);
|
67
|
+
}
|
68
|
+
currentNavigation = directoryMap[currentPath].children;
|
69
|
+
}
|
70
|
+
currentNavigation.push(item);
|
71
|
+
}
|
72
|
+
}
|
73
|
+
// Sort navigation items by order
|
74
|
+
function sortNavigation(items) {
|
75
|
+
items.sort((a, b) => a.order - b.order);
|
76
|
+
for (const item of items) {
|
77
|
+
if (item.children && item.children.length > 0) {
|
78
|
+
sortNavigation(item.children);
|
79
|
+
}
|
80
|
+
}
|
81
|
+
}
|
82
|
+
sortNavigation(navigation);
|
83
|
+
return {
|
84
|
+
content: [{ type: "text", text: JSON.stringify(navigation, null, 2) }],
|
85
|
+
};
|
86
|
+
}
|
87
|
+
catch (error) {
|
88
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
89
|
+
return {
|
90
|
+
content: [
|
91
|
+
{
|
92
|
+
type: "text",
|
93
|
+
text: `Error generating navigation: ${errorMessage}`,
|
94
|
+
},
|
95
|
+
],
|
96
|
+
isError: true,
|
97
|
+
};
|
98
|
+
}
|
99
|
+
}
|
100
|
+
/**
|
101
|
+
* Save navigation structure to a file
|
102
|
+
*/
|
103
|
+
async saveNavigation(basePath = "", outputPath = "navigation.json") {
|
104
|
+
try {
|
105
|
+
const result = await this.generateNavigation(basePath);
|
106
|
+
if (result.isError) {
|
107
|
+
return result;
|
108
|
+
}
|
109
|
+
const navigation = JSON.parse(result.content[0].text);
|
110
|
+
const outputFilePath = path.join(this.docsDir, outputPath);
|
111
|
+
await fs.writeFile(outputFilePath, JSON.stringify(navigation, null, 2), "utf-8");
|
112
|
+
return {
|
113
|
+
content: [
|
114
|
+
{ type: "text", text: `Navigation structure saved to ${outputPath}` },
|
115
|
+
],
|
116
|
+
};
|
117
|
+
}
|
118
|
+
catch (error) {
|
119
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
120
|
+
return {
|
121
|
+
content: [
|
122
|
+
{ type: "text", text: `Error saving navigation: ${errorMessage}` },
|
123
|
+
],
|
124
|
+
isError: true,
|
125
|
+
};
|
126
|
+
}
|
127
|
+
}
|
128
|
+
}
|