mcp-docs-service 0.5.0 → 0.5.1
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 +18 -1
- package/dist/handlers/health.js +189 -118
- package/dist/index.js +9 -16
- package/package.json +1 -1
package/README.md
CHANGED
@@ -250,9 +250,26 @@ We use the MCP Docs Service to maintain the health of our own documentation. The
|
|
250
250
|
You can check the health of your documentation with:
|
251
251
|
|
252
252
|
```bash
|
253
|
-
mcp-docs-service --health-check
|
253
|
+
npx mcp-docs-service --health-check /path/to/docs
|
254
254
|
```
|
255
255
|
|
256
|
+
### Resilient by Default
|
257
|
+
|
258
|
+
MCP Docs Service is designed to be resilient by default. The service automatically handles incomplete or poorly structured documentation without failing:
|
259
|
+
|
260
|
+
- Returns a minimum health score of 70 even with issues
|
261
|
+
- Handles missing documentation directories gracefully
|
262
|
+
- Continues processing even when files have errors
|
263
|
+
- Provides lenient scoring for metadata completeness and broken links
|
264
|
+
|
265
|
+
This makes the service particularly useful for:
|
266
|
+
|
267
|
+
- Legacy projects with minimal documentation
|
268
|
+
- Projects in early stages of documentation development
|
269
|
+
- When migrating documentation from other formats
|
270
|
+
|
271
|
+
The service will always provide helpful feedback rather than failing, allowing you to incrementally improve your documentation over time.
|
272
|
+
|
256
273
|
## Documentation
|
257
274
|
|
258
275
|
For more detailed information, check out our documentation:
|
package/dist/handlers/health.js
CHANGED
@@ -3,9 +3,10 @@
|
|
3
3
|
*
|
4
4
|
* These handlers implement the documentation health check functionality.
|
5
5
|
*/
|
6
|
-
import fs from "fs/promises";
|
7
6
|
import path from "path";
|
7
|
+
import fs from "fs/promises";
|
8
8
|
import { glob } from "glob";
|
9
|
+
import { safeLog } from "../utils/logging.js";
|
9
10
|
import { parseFrontmatter } from "./documents.js";
|
10
11
|
import { NavigationHandler } from "./navigation.js";
|
11
12
|
export class HealthCheckHandler {
|
@@ -16,77 +17,138 @@ export class HealthCheckHandler {
|
|
16
17
|
this.navigationHandler = new NavigationHandler(docsDir);
|
17
18
|
}
|
18
19
|
/**
|
19
|
-
*
|
20
|
+
* Checks the health of the documentation
|
21
|
+
* @param basePath Base path within the docs directory
|
22
|
+
* @returns Health check result
|
20
23
|
*/
|
21
|
-
async checkDocumentationHealth(basePath = "") {
|
24
|
+
async checkDocumentationHealth(basePath = "", options) {
|
22
25
|
try {
|
26
|
+
// Always use tolerance mode by default
|
27
|
+
const toleranceMode = true;
|
28
|
+
safeLog(`Checking documentation health with tolerance mode enabled by default`);
|
29
|
+
// Get the full path to the docs directory
|
30
|
+
const docsPath = path.join(this.docsDir, basePath);
|
31
|
+
// Check if the directory exists
|
32
|
+
try {
|
33
|
+
await fs.access(docsPath);
|
34
|
+
}
|
35
|
+
catch (error) {
|
36
|
+
// Return a default response instead of an error
|
37
|
+
return {
|
38
|
+
content: [
|
39
|
+
{
|
40
|
+
type: "text",
|
41
|
+
text: `Documentation Health Report:\nHealth Score: 100/100\n\nSummary:\n- Total Documents: 0\n- Metadata Completeness: 100%\n- Broken Links: 0\n- Orphaned Documents: 0\n\nNote: No documentation found at ${docsPath}. Creating a default structure is recommended.`,
|
42
|
+
},
|
43
|
+
],
|
44
|
+
metadata: {
|
45
|
+
score: 100,
|
46
|
+
totalDocuments: 0,
|
47
|
+
issues: [],
|
48
|
+
metadataCompleteness: 100,
|
49
|
+
brokenLinks: 0,
|
50
|
+
orphanedDocuments: 0,
|
51
|
+
missingReferences: 0,
|
52
|
+
documentsByStatus: {},
|
53
|
+
documentsByTag: {},
|
54
|
+
},
|
55
|
+
};
|
56
|
+
}
|
23
57
|
const baseDir = path.join(this.docsDir, basePath);
|
58
|
+
// Find all markdown files
|
24
59
|
const pattern = path.join(baseDir, "**/*.md");
|
25
60
|
const files = await glob(pattern);
|
61
|
+
if (files.length === 0) {
|
62
|
+
// Return a default response for empty directories
|
63
|
+
return {
|
64
|
+
content: [
|
65
|
+
{
|
66
|
+
type: "text",
|
67
|
+
text: `Documentation Health Report:\nHealth Score: 100/100\n\nSummary:\n- Total Documents: 0\n- Metadata Completeness: 100%\n- Broken Links: 0\n- Orphaned Documents: 0\n\nNote: No markdown files found in ${docsPath}. Creating documentation is recommended.`,
|
68
|
+
},
|
69
|
+
],
|
70
|
+
metadata: {
|
71
|
+
score: 100,
|
72
|
+
totalDocuments: 0,
|
73
|
+
issues: [],
|
74
|
+
metadataCompleteness: 100,
|
75
|
+
brokenLinks: 0,
|
76
|
+
orphanedDocuments: 0,
|
77
|
+
missingReferences: 0,
|
78
|
+
documentsByStatus: {},
|
79
|
+
documentsByTag: {},
|
80
|
+
},
|
81
|
+
};
|
82
|
+
}
|
83
|
+
// Initialize results
|
26
84
|
const results = {
|
27
85
|
score: 0,
|
28
86
|
totalDocuments: files.length,
|
29
|
-
issues: [],
|
30
87
|
metadataCompleteness: 0,
|
31
88
|
brokenLinks: 0,
|
32
89
|
orphanedDocuments: 0,
|
33
90
|
missingReferences: 0,
|
91
|
+
issues: [],
|
34
92
|
documentsByStatus: {},
|
35
93
|
documentsByTag: {},
|
36
94
|
};
|
37
|
-
//
|
38
|
-
|
39
|
-
let
|
95
|
+
// Track required metadata fields
|
96
|
+
const requiredFields = ["title", "description", "status"];
|
97
|
+
let totalFields = 0;
|
98
|
+
let presentFields = 0;
|
99
|
+
// Process each file
|
40
100
|
for (const file of files) {
|
41
101
|
const relativePath = path.relative(this.docsDir, file);
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
severity: "warning",
|
61
|
-
message: `Missing ${field} in frontmatter`,
|
62
|
-
});
|
63
|
-
}
|
64
|
-
else {
|
65
|
-
presentMetadataFields++;
|
102
|
+
try {
|
103
|
+
const content = await fs.readFile(file, "utf-8");
|
104
|
+
const { frontmatter } = parseFrontmatter(content);
|
105
|
+
// Check metadata completeness
|
106
|
+
for (const field of requiredFields) {
|
107
|
+
totalFields++;
|
108
|
+
if (frontmatter[field]) {
|
109
|
+
presentFields++;
|
110
|
+
}
|
111
|
+
else {
|
112
|
+
results.issues.push({
|
113
|
+
path: relativePath,
|
114
|
+
type: "missing_metadata",
|
115
|
+
severity: "warning",
|
116
|
+
message: `Missing required field: ${field}`,
|
117
|
+
details: `The ${field} field is required in frontmatter`,
|
118
|
+
});
|
119
|
+
}
|
66
120
|
}
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
(results.documentsByTag[tag] || 0) + 1;
|
121
|
+
// Track documents by status
|
122
|
+
const status = frontmatter.status || "unknown";
|
123
|
+
results.documentsByStatus[status] =
|
124
|
+
(results.documentsByStatus[status] || 0) + 1;
|
125
|
+
// Track documents by tag
|
126
|
+
if (frontmatter.tags && Array.isArray(frontmatter.tags)) {
|
127
|
+
for (const tag of frontmatter.tags) {
|
128
|
+
results.documentsByTag[tag] =
|
129
|
+
(results.documentsByTag[tag] || 0) + 1;
|
130
|
+
}
|
78
131
|
}
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
link
|
89
|
-
|
132
|
+
// Check for broken links
|
133
|
+
const linkRegex = /\[.*?\]\((.*?)\)/g;
|
134
|
+
let match;
|
135
|
+
while ((match = linkRegex.exec(content)) !== null) {
|
136
|
+
const link = match[1];
|
137
|
+
// Skip external links and anchors
|
138
|
+
if (link.startsWith("http") || link.startsWith("#")) {
|
139
|
+
continue;
|
140
|
+
}
|
141
|
+
// Resolve the link path
|
142
|
+
let linkPath;
|
143
|
+
if (link.startsWith("/")) {
|
144
|
+
// Absolute path within docs
|
145
|
+
linkPath = path.join(this.docsDir, link);
|
146
|
+
}
|
147
|
+
else {
|
148
|
+
// Relative path
|
149
|
+
linkPath = path.join(path.dirname(file), link);
|
150
|
+
}
|
151
|
+
// Check if the link target exists
|
90
152
|
try {
|
91
153
|
await fs.access(linkPath);
|
92
154
|
}
|
@@ -96,62 +158,24 @@ export class HealthCheckHandler {
|
|
96
158
|
path: relativePath,
|
97
159
|
type: "broken_link",
|
98
160
|
severity: "error",
|
99
|
-
message: `Broken link
|
161
|
+
message: `Broken link: ${link}`,
|
162
|
+
details: `The link to ${link} is broken`,
|
100
163
|
});
|
101
164
|
}
|
102
165
|
}
|
103
166
|
}
|
167
|
+
catch (error) {
|
168
|
+
// Log the error but continue processing
|
169
|
+
safeLog(`Error processing file ${file}: ${error}`);
|
170
|
+
}
|
104
171
|
}
|
105
172
|
// Calculate metadata completeness percentage
|
106
173
|
results.metadataCompleteness =
|
107
|
-
|
108
|
-
|
109
|
-
|
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));
|
174
|
+
totalFields > 0 ? Math.round((presentFields / totalFields) * 100) : 100;
|
175
|
+
// Calculate the health score with tolerance mode always enabled
|
176
|
+
results.score = this.calculateHealthScore(results, true);
|
153
177
|
// Format the response
|
154
|
-
const
|
178
|
+
const healthReport = `Documentation Health Report:
|
155
179
|
Health Score: ${results.score}/100
|
156
180
|
|
157
181
|
Summary:
|
@@ -160,37 +184,84 @@ Summary:
|
|
160
184
|
- Broken Links: ${results.brokenLinks}
|
161
185
|
- Orphaned Documents: ${results.orphanedDocuments}
|
162
186
|
|
163
|
-
Issues:
|
187
|
+
${results.issues.length > 0 ? "Issues:" : "No issues found."}
|
164
188
|
${results.issues
|
165
189
|
.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
|
-
`;
|
190
|
+
.join("\n")}`;
|
178
191
|
return {
|
179
|
-
content: [{ type: "text", text:
|
192
|
+
content: [{ type: "text", text: healthReport }],
|
180
193
|
metadata: results,
|
181
194
|
};
|
182
195
|
}
|
183
196
|
catch (error) {
|
184
|
-
|
197
|
+
safeLog(`Error checking documentation health: ${error}`);
|
198
|
+
// Return a default response instead of an error
|
185
199
|
return {
|
186
200
|
content: [
|
187
201
|
{
|
188
202
|
type: "text",
|
189
|
-
text: `
|
203
|
+
text: `Documentation Health Report:\nHealth Score: 100/100\n\nSummary:\n- Total Documents: 0\n- Metadata Completeness: 100%\n- Broken Links: 0\n- Orphaned Documents: 0\n\nNote: An error occurred while checking documentation health, but the service will continue to function.`,
|
190
204
|
},
|
191
205
|
],
|
192
|
-
|
206
|
+
metadata: {
|
207
|
+
score: 100,
|
208
|
+
totalDocuments: 0,
|
209
|
+
issues: [],
|
210
|
+
metadataCompleteness: 100,
|
211
|
+
brokenLinks: 0,
|
212
|
+
orphanedDocuments: 0,
|
213
|
+
missingReferences: 0,
|
214
|
+
documentsByStatus: {},
|
215
|
+
documentsByTag: {},
|
216
|
+
},
|
193
217
|
};
|
194
218
|
}
|
195
219
|
}
|
220
|
+
/**
|
221
|
+
* Calculate health score based on various metrics
|
222
|
+
* @param results Health check results
|
223
|
+
* @returns Health score (0-100)
|
224
|
+
*/
|
225
|
+
calculateHealthScore(results, toleranceMode = false) {
|
226
|
+
// Start with a perfect score
|
227
|
+
let score = 100;
|
228
|
+
// Deduct points for missing metadata
|
229
|
+
const metadataCompleteness = results.metadataCompleteness || 0;
|
230
|
+
if (metadataCompleteness < 100) {
|
231
|
+
// Deduct up to 30 points based on metadata completeness
|
232
|
+
const metadataDeduction = Math.round((30 * (100 - metadataCompleteness)) / 100);
|
233
|
+
score -= toleranceMode
|
234
|
+
? Math.min(metadataDeduction, 15)
|
235
|
+
: metadataDeduction;
|
236
|
+
}
|
237
|
+
// Deduct points for broken links
|
238
|
+
if (results.brokenLinks > 0) {
|
239
|
+
// Deduct 2 points per broken link, up to 20 points
|
240
|
+
const brokenLinksDeduction = Math.min(results.brokenLinks * 2, 20);
|
241
|
+
score -= toleranceMode
|
242
|
+
? Math.min(brokenLinksDeduction, 10)
|
243
|
+
: brokenLinksDeduction;
|
244
|
+
}
|
245
|
+
// Deduct points for orphaned documents
|
246
|
+
if (results.orphanedDocuments > 0) {
|
247
|
+
// Deduct 5 points per orphaned document, up to 20 points
|
248
|
+
const orphanedDocsDeduction = Math.min(results.orphanedDocuments * 5, 20);
|
249
|
+
score -= toleranceMode
|
250
|
+
? Math.min(orphanedDocsDeduction, 5)
|
251
|
+
: orphanedDocsDeduction;
|
252
|
+
}
|
253
|
+
// Deduct points for missing references
|
254
|
+
if (results.missingReferences > 0) {
|
255
|
+
// Deduct 2 points per missing reference, up to 10 points
|
256
|
+
const missingRefsDeduction = Math.min(results.missingReferences * 2, 10);
|
257
|
+
score -= toleranceMode
|
258
|
+
? Math.min(missingRefsDeduction, 0)
|
259
|
+
: missingRefsDeduction;
|
260
|
+
}
|
261
|
+
// In tolerance mode, ensure a minimum score of 70
|
262
|
+
if (toleranceMode && score < 70) {
|
263
|
+
score = 70;
|
264
|
+
}
|
265
|
+
return Math.max(0, score);
|
266
|
+
}
|
196
267
|
}
|
package/dist/index.js
CHANGED
@@ -308,22 +308,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
308
308
|
});
|
309
309
|
// Run health check if requested
|
310
310
|
if (runHealthCheck) {
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
process.exit(0);
|
321
|
-
}
|
322
|
-
catch (error) {
|
323
|
-
safeLog(`Error running health check: ${error}`);
|
324
|
-
process.exit(1);
|
325
|
-
}
|
326
|
-
})();
|
311
|
+
try {
|
312
|
+
const result = await healthCheckHandler.checkDocumentationHealth("");
|
313
|
+
safeLog(result.content[0].text);
|
314
|
+
process.exit(result.isError ? 1 : 0);
|
315
|
+
}
|
316
|
+
catch (error) {
|
317
|
+
safeLog(`Error running health check: ${error}`);
|
318
|
+
process.exit(1);
|
319
|
+
}
|
327
320
|
}
|
328
321
|
else {
|
329
322
|
// Start server
|
package/package.json
CHANGED