docusaurus-plugin-mcp-server 0.5.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/LICENSE +21 -0
- package/README.md +505 -0
- package/dist/adapters-entry.d.mts +180 -0
- package/dist/adapters-entry.d.ts +180 -0
- package/dist/adapters-entry.js +918 -0
- package/dist/adapters-entry.js.map +1 -0
- package/dist/adapters-entry.mjs +908 -0
- package/dist/adapters-entry.mjs.map +1 -0
- package/dist/cli/verify.d.mts +1 -0
- package/dist/cli/verify.d.ts +1 -0
- package/dist/cli/verify.js +710 -0
- package/dist/cli/verify.js.map +1 -0
- package/dist/cli/verify.mjs +702 -0
- package/dist/cli/verify.mjs.map +1 -0
- package/dist/index-CzA4FjeE.d.mts +190 -0
- package/dist/index-CzA4FjeE.d.ts +190 -0
- package/dist/index.d.mts +234 -0
- package/dist/index.d.ts +234 -0
- package/dist/index.js +1070 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1037 -0
- package/dist/index.mjs.map +1 -0
- package/dist/theme/index.d.mts +87 -0
- package/dist/theme/index.d.ts +87 -0
- package/dist/theme/index.js +299 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme/index.mjs +291 -0
- package/dist/theme/index.mjs.map +1 -0
- package/package.json +145 -0
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs2 from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
6
|
+
import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import FlexSearch from 'flexsearch';
|
|
9
|
+
|
|
10
|
+
var FIELD_WEIGHTS = {
|
|
11
|
+
title: 3,
|
|
12
|
+
headings: 2,
|
|
13
|
+
description: 1.5,
|
|
14
|
+
content: 1
|
|
15
|
+
};
|
|
16
|
+
function englishStemmer(word) {
|
|
17
|
+
if (word.length <= 3) return word;
|
|
18
|
+
return word.replace(/ing$/, "").replace(/tion$/, "t").replace(/sion$/, "s").replace(/([^aeiou])ed$/, "$1").replace(/([^aeiou])es$/, "$1").replace(/ly$/, "").replace(/ment$/, "").replace(/ness$/, "").replace(/ies$/, "y").replace(/([^s])s$/, "$1");
|
|
19
|
+
}
|
|
20
|
+
function createSearchIndex() {
|
|
21
|
+
return new FlexSearch.Document({
|
|
22
|
+
// Use 'full' tokenization for substring matching
|
|
23
|
+
// This allows "auth" to match "authentication"
|
|
24
|
+
tokenize: "full",
|
|
25
|
+
// Enable caching for faster repeated queries
|
|
26
|
+
cache: 100,
|
|
27
|
+
// Higher resolution = more granular ranking (1-9)
|
|
28
|
+
resolution: 9,
|
|
29
|
+
// Enable context for phrase/proximity matching
|
|
30
|
+
context: {
|
|
31
|
+
resolution: 2,
|
|
32
|
+
depth: 2,
|
|
33
|
+
bidirectional: true
|
|
34
|
+
},
|
|
35
|
+
// Apply stemming to normalize word forms
|
|
36
|
+
encode: (str) => {
|
|
37
|
+
const words = str.toLowerCase().split(/[\s\-_.,;:!?'"()[\]{}]+/);
|
|
38
|
+
return words.filter(Boolean).map(englishStemmer);
|
|
39
|
+
},
|
|
40
|
+
// Document schema
|
|
41
|
+
document: {
|
|
42
|
+
id: "id",
|
|
43
|
+
// Index these fields for searching
|
|
44
|
+
index: ["title", "content", "headings", "description"],
|
|
45
|
+
// Store these fields in results (for enriched queries)
|
|
46
|
+
store: ["title", "description"]
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function searchIndex(index, docs, query, options = {}) {
|
|
51
|
+
const { limit = 5 } = options;
|
|
52
|
+
const rawResults = index.search(query, {
|
|
53
|
+
limit: limit * 3,
|
|
54
|
+
// Get extra results for better ranking after weighting
|
|
55
|
+
enrich: true
|
|
56
|
+
});
|
|
57
|
+
const docScores = /* @__PURE__ */ new Map();
|
|
58
|
+
for (const fieldResult of rawResults) {
|
|
59
|
+
const field = fieldResult.field;
|
|
60
|
+
const fieldWeight = FIELD_WEIGHTS[field] ?? 1;
|
|
61
|
+
const results2 = fieldResult.result;
|
|
62
|
+
for (let i = 0; i < results2.length; i++) {
|
|
63
|
+
const item = results2[i];
|
|
64
|
+
if (!item) continue;
|
|
65
|
+
const docId = typeof item === "string" ? item : item.id;
|
|
66
|
+
const positionScore = (results2.length - i) / results2.length;
|
|
67
|
+
const weightedScore = positionScore * fieldWeight;
|
|
68
|
+
const existingScore = docScores.get(docId) ?? 0;
|
|
69
|
+
docScores.set(docId, existingScore + weightedScore);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const results = [];
|
|
73
|
+
for (const [docId, score] of docScores) {
|
|
74
|
+
const doc = docs[docId];
|
|
75
|
+
if (!doc) continue;
|
|
76
|
+
results.push({
|
|
77
|
+
route: doc.route,
|
|
78
|
+
title: doc.title,
|
|
79
|
+
score,
|
|
80
|
+
snippet: generateSnippet(doc.markdown, query),
|
|
81
|
+
matchingHeadings: findMatchingHeadings(doc, query)
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
results.sort((a, b) => b.score - a.score);
|
|
85
|
+
return results.slice(0, limit);
|
|
86
|
+
}
|
|
87
|
+
function generateSnippet(markdown, query) {
|
|
88
|
+
const maxLength = 200;
|
|
89
|
+
const queryTerms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
90
|
+
if (queryTerms.length === 0) {
|
|
91
|
+
return markdown.slice(0, maxLength) + (markdown.length > maxLength ? "..." : "");
|
|
92
|
+
}
|
|
93
|
+
const lowerMarkdown = markdown.toLowerCase();
|
|
94
|
+
let bestIndex = -1;
|
|
95
|
+
let bestTerm = "";
|
|
96
|
+
const allTerms = [...queryTerms, ...queryTerms.map(englishStemmer)];
|
|
97
|
+
for (const term of allTerms) {
|
|
98
|
+
const index = lowerMarkdown.indexOf(term);
|
|
99
|
+
if (index !== -1 && (bestIndex === -1 || index < bestIndex)) {
|
|
100
|
+
bestIndex = index;
|
|
101
|
+
bestTerm = term;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (bestIndex === -1) {
|
|
105
|
+
return markdown.slice(0, maxLength) + (markdown.length > maxLength ? "..." : "");
|
|
106
|
+
}
|
|
107
|
+
const snippetStart = Math.max(0, bestIndex - 50);
|
|
108
|
+
const snippetEnd = Math.min(markdown.length, bestIndex + bestTerm.length + 150);
|
|
109
|
+
let snippet = markdown.slice(snippetStart, snippetEnd);
|
|
110
|
+
snippet = snippet.replace(/^#{1,6}\s+/gm, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/!\[([^\]]*)\]\([^)]+\)/g, "").replace(/```[a-z]*\n?/g, "").replace(/`([^`]+)`/g, "$1").replace(/\s+/g, " ").trim();
|
|
111
|
+
const prefix = snippetStart > 0 ? "..." : "";
|
|
112
|
+
const suffix = snippetEnd < markdown.length ? "..." : "";
|
|
113
|
+
return prefix + snippet + suffix;
|
|
114
|
+
}
|
|
115
|
+
function findMatchingHeadings(doc, query) {
|
|
116
|
+
const queryTerms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
117
|
+
const allTerms = [...queryTerms, ...queryTerms.map(englishStemmer)];
|
|
118
|
+
const matching = [];
|
|
119
|
+
for (const heading of doc.headings) {
|
|
120
|
+
const headingLower = heading.text.toLowerCase();
|
|
121
|
+
const headingStemmed = headingLower.split(/\s+/).map(englishStemmer).join(" ");
|
|
122
|
+
if (allTerms.some(
|
|
123
|
+
(term) => headingLower.includes(term) || headingStemmed.includes(englishStemmer(term))
|
|
124
|
+
)) {
|
|
125
|
+
matching.push(heading.text);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return matching.slice(0, 3);
|
|
129
|
+
}
|
|
130
|
+
async function importSearchIndex(data) {
|
|
131
|
+
const index = createSearchIndex();
|
|
132
|
+
for (const [key, value] of Object.entries(data)) {
|
|
133
|
+
await index.import(
|
|
134
|
+
key,
|
|
135
|
+
value
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return index;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/mcp/tools/docs-search.ts
|
|
142
|
+
function executeDocsSearch(params, index, docs) {
|
|
143
|
+
const { query, limit = 5 } = params;
|
|
144
|
+
if (!query || typeof query !== "string" || query.trim().length === 0) {
|
|
145
|
+
throw new Error("Query parameter is required and must be a non-empty string");
|
|
146
|
+
}
|
|
147
|
+
const effectiveLimit = Math.min(Math.max(1, limit), 20);
|
|
148
|
+
const results = searchIndex(index, docs, query.trim(), {
|
|
149
|
+
limit: effectiveLimit
|
|
150
|
+
});
|
|
151
|
+
return results;
|
|
152
|
+
}
|
|
153
|
+
function formatSearchResults(results, baseUrl) {
|
|
154
|
+
if (results.length === 0) {
|
|
155
|
+
return "No matching documents found.";
|
|
156
|
+
}
|
|
157
|
+
const lines = [`Found ${results.length} result(s):
|
|
158
|
+
`];
|
|
159
|
+
for (let i = 0; i < results.length; i++) {
|
|
160
|
+
const result = results[i];
|
|
161
|
+
if (!result) continue;
|
|
162
|
+
lines.push(`${i + 1}. **${result.title}**`);
|
|
163
|
+
if (baseUrl) {
|
|
164
|
+
const fullUrl = `${baseUrl.replace(/\/$/, "")}${result.route}`;
|
|
165
|
+
lines.push(` URL: ${fullUrl}`);
|
|
166
|
+
}
|
|
167
|
+
lines.push(` Route: ${result.route}`);
|
|
168
|
+
if (result.matchingHeadings && result.matchingHeadings.length > 0) {
|
|
169
|
+
lines.push(` Matching sections: ${result.matchingHeadings.join(", ")}`);
|
|
170
|
+
}
|
|
171
|
+
lines.push(` ${result.snippet}`);
|
|
172
|
+
lines.push("");
|
|
173
|
+
}
|
|
174
|
+
return lines.join("\n");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/mcp/tools/docs-get-page.ts
|
|
178
|
+
function executeDocsGetPage(params, docs) {
|
|
179
|
+
const { route } = params;
|
|
180
|
+
if (!route || typeof route !== "string") {
|
|
181
|
+
throw new Error("Route parameter is required and must be a string");
|
|
182
|
+
}
|
|
183
|
+
let normalizedRoute = route.trim();
|
|
184
|
+
if (!normalizedRoute.startsWith("/")) {
|
|
185
|
+
normalizedRoute = "/" + normalizedRoute;
|
|
186
|
+
}
|
|
187
|
+
if (normalizedRoute.length > 1 && normalizedRoute.endsWith("/")) {
|
|
188
|
+
normalizedRoute = normalizedRoute.slice(0, -1);
|
|
189
|
+
}
|
|
190
|
+
const doc = docs[normalizedRoute];
|
|
191
|
+
if (!doc) {
|
|
192
|
+
const altRoute = normalizedRoute.slice(1);
|
|
193
|
+
if (docs[altRoute]) {
|
|
194
|
+
return docs[altRoute] ?? null;
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
return doc;
|
|
199
|
+
}
|
|
200
|
+
function formatPageContent(doc, baseUrl) {
|
|
201
|
+
if (!doc) {
|
|
202
|
+
return "Page not found. Please check the route path and try again.";
|
|
203
|
+
}
|
|
204
|
+
const lines = [];
|
|
205
|
+
lines.push(`# ${doc.title}`);
|
|
206
|
+
lines.push("");
|
|
207
|
+
if (doc.description) {
|
|
208
|
+
lines.push(`> ${doc.description}`);
|
|
209
|
+
lines.push("");
|
|
210
|
+
}
|
|
211
|
+
if (baseUrl) {
|
|
212
|
+
const fullUrl = `${baseUrl.replace(/\/$/, "")}${doc.route}`;
|
|
213
|
+
lines.push(`**URL:** ${fullUrl}`);
|
|
214
|
+
}
|
|
215
|
+
lines.push(`**Route:** ${doc.route}`);
|
|
216
|
+
lines.push("");
|
|
217
|
+
if (doc.headings.length > 0) {
|
|
218
|
+
lines.push("## Contents");
|
|
219
|
+
lines.push("");
|
|
220
|
+
for (const heading of doc.headings) {
|
|
221
|
+
if (heading.level <= 3) {
|
|
222
|
+
const indent = " ".repeat(heading.level - 1);
|
|
223
|
+
lines.push(`${indent}- [${heading.text}](#${heading.id})`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
lines.push("");
|
|
227
|
+
lines.push("---");
|
|
228
|
+
lines.push("");
|
|
229
|
+
}
|
|
230
|
+
lines.push(doc.markdown);
|
|
231
|
+
return lines.join("\n");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/processing/heading-extractor.ts
|
|
235
|
+
function extractSection(markdown, headingId, headings) {
|
|
236
|
+
const heading = headings.find((h) => h.id === headingId);
|
|
237
|
+
if (!heading) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
return markdown.slice(heading.startOffset, heading.endOffset).trim();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/mcp/tools/docs-get-section.ts
|
|
244
|
+
function executeDocsGetSection(params, docs) {
|
|
245
|
+
const { route, headingId } = params;
|
|
246
|
+
if (!route || typeof route !== "string") {
|
|
247
|
+
throw new Error("Route parameter is required and must be a string");
|
|
248
|
+
}
|
|
249
|
+
if (!headingId || typeof headingId !== "string") {
|
|
250
|
+
throw new Error("HeadingId parameter is required and must be a string");
|
|
251
|
+
}
|
|
252
|
+
let normalizedRoute = route.trim();
|
|
253
|
+
if (!normalizedRoute.startsWith("/")) {
|
|
254
|
+
normalizedRoute = "/" + normalizedRoute;
|
|
255
|
+
}
|
|
256
|
+
if (normalizedRoute.length > 1 && normalizedRoute.endsWith("/")) {
|
|
257
|
+
normalizedRoute = normalizedRoute.slice(0, -1);
|
|
258
|
+
}
|
|
259
|
+
const doc = docs[normalizedRoute];
|
|
260
|
+
if (!doc) {
|
|
261
|
+
return {
|
|
262
|
+
content: null,
|
|
263
|
+
doc: null,
|
|
264
|
+
headingText: null,
|
|
265
|
+
availableHeadings: []
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
const availableHeadings = doc.headings.map((h) => ({
|
|
269
|
+
id: h.id,
|
|
270
|
+
text: h.text,
|
|
271
|
+
level: h.level
|
|
272
|
+
}));
|
|
273
|
+
const heading = doc.headings.find((h) => h.id === headingId.trim());
|
|
274
|
+
if (!heading) {
|
|
275
|
+
return {
|
|
276
|
+
content: null,
|
|
277
|
+
doc,
|
|
278
|
+
headingText: null,
|
|
279
|
+
availableHeadings
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
const content = extractSection(doc.markdown, headingId.trim(), doc.headings);
|
|
283
|
+
return {
|
|
284
|
+
content,
|
|
285
|
+
doc,
|
|
286
|
+
headingText: heading.text,
|
|
287
|
+
availableHeadings
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function formatSectionContent(result, headingId, baseUrl) {
|
|
291
|
+
if (!result.doc) {
|
|
292
|
+
return "Page not found. Please check the route path and try again.";
|
|
293
|
+
}
|
|
294
|
+
if (!result.content) {
|
|
295
|
+
const lines2 = [`Section "${headingId}" not found in this document.`, "", "Available sections:"];
|
|
296
|
+
for (const heading of result.availableHeadings) {
|
|
297
|
+
const indent = " ".repeat(heading.level - 1);
|
|
298
|
+
lines2.push(`${indent}- ${heading.text} (id: ${heading.id})`);
|
|
299
|
+
}
|
|
300
|
+
return lines2.join("\n");
|
|
301
|
+
}
|
|
302
|
+
const lines = [];
|
|
303
|
+
const fullUrl = baseUrl ? `${baseUrl.replace(/\/$/, "")}${result.doc.route}#${headingId}` : null;
|
|
304
|
+
lines.push(`# ${result.headingText}`);
|
|
305
|
+
if (fullUrl) {
|
|
306
|
+
lines.push(`> From: ${result.doc.title} - ${fullUrl}`);
|
|
307
|
+
} else {
|
|
308
|
+
lines.push(`> From: ${result.doc.title} (${result.doc.route})`);
|
|
309
|
+
}
|
|
310
|
+
lines.push("");
|
|
311
|
+
lines.push("---");
|
|
312
|
+
lines.push("");
|
|
313
|
+
lines.push(result.content);
|
|
314
|
+
return lines.join("\n");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/mcp/server.ts
|
|
318
|
+
function isFileConfig(config) {
|
|
319
|
+
return "docsPath" in config && "indexPath" in config;
|
|
320
|
+
}
|
|
321
|
+
function isDataConfig(config) {
|
|
322
|
+
return "docs" in config && "searchIndexData" in config;
|
|
323
|
+
}
|
|
324
|
+
var McpDocsServer = class {
|
|
325
|
+
config;
|
|
326
|
+
docs = null;
|
|
327
|
+
searchIndex = null;
|
|
328
|
+
mcpServer;
|
|
329
|
+
initialized = false;
|
|
330
|
+
constructor(config) {
|
|
331
|
+
this.config = config;
|
|
332
|
+
this.mcpServer = new McpServer(
|
|
333
|
+
{
|
|
334
|
+
name: config.name,
|
|
335
|
+
version: config.version ?? "1.0.0"
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
capabilities: {
|
|
339
|
+
tools: {}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
);
|
|
343
|
+
this.registerTools();
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Register all MCP tools using the SDK's registerTool API
|
|
347
|
+
*/
|
|
348
|
+
registerTools() {
|
|
349
|
+
this.mcpServer.registerTool(
|
|
350
|
+
"docs_search",
|
|
351
|
+
{
|
|
352
|
+
description: "Search the documentation for relevant pages. Returns matching documents with snippets and relevance scores. Use this to find information across all documentation.",
|
|
353
|
+
inputSchema: {
|
|
354
|
+
query: z.string().min(1).describe("The search query string"),
|
|
355
|
+
limit: z.number().int().min(1).max(20).optional().default(5).describe("Maximum number of results to return (1-20, default: 5)")
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
async ({ query, limit }) => {
|
|
359
|
+
await this.initialize();
|
|
360
|
+
if (!this.docs || !this.searchIndex) {
|
|
361
|
+
return {
|
|
362
|
+
content: [{ type: "text", text: "Server not initialized. Please try again." }],
|
|
363
|
+
isError: true
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
const results = executeDocsSearch({ query, limit }, this.searchIndex, this.docs);
|
|
367
|
+
return {
|
|
368
|
+
content: [
|
|
369
|
+
{ type: "text", text: formatSearchResults(results, this.config.baseUrl) }
|
|
370
|
+
]
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
);
|
|
374
|
+
this.mcpServer.registerTool(
|
|
375
|
+
"docs_get_page",
|
|
376
|
+
{
|
|
377
|
+
description: "Retrieve the complete content of a documentation page as markdown. Use this when you need the full content of a specific page.",
|
|
378
|
+
inputSchema: {
|
|
379
|
+
route: z.string().min(1).describe('The page route path (e.g., "/docs/getting-started" or "/api/reference")')
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
async ({ route }) => {
|
|
383
|
+
await this.initialize();
|
|
384
|
+
if (!this.docs) {
|
|
385
|
+
return {
|
|
386
|
+
content: [{ type: "text", text: "Server not initialized. Please try again." }],
|
|
387
|
+
isError: true
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
const doc = executeDocsGetPage({ route }, this.docs);
|
|
391
|
+
return {
|
|
392
|
+
content: [{ type: "text", text: formatPageContent(doc, this.config.baseUrl) }]
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
);
|
|
396
|
+
this.mcpServer.registerTool(
|
|
397
|
+
"docs_get_section",
|
|
398
|
+
{
|
|
399
|
+
description: "Retrieve a specific section from a documentation page by its heading ID. Use this when you need only a portion of a page rather than the entire content.",
|
|
400
|
+
inputSchema: {
|
|
401
|
+
route: z.string().min(1).describe("The page route path"),
|
|
402
|
+
headingId: z.string().min(1).describe(
|
|
403
|
+
'The heading ID of the section to extract (e.g., "installation", "api-reference")'
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
async ({ route, headingId }) => {
|
|
408
|
+
await this.initialize();
|
|
409
|
+
if (!this.docs) {
|
|
410
|
+
return {
|
|
411
|
+
content: [{ type: "text", text: "Server not initialized. Please try again." }],
|
|
412
|
+
isError: true
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
const result = executeDocsGetSection({ route, headingId }, this.docs);
|
|
416
|
+
return {
|
|
417
|
+
content: [
|
|
418
|
+
{
|
|
419
|
+
type: "text",
|
|
420
|
+
text: formatSectionContent(result, headingId, this.config.baseUrl)
|
|
421
|
+
}
|
|
422
|
+
]
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Load docs and search index
|
|
429
|
+
*
|
|
430
|
+
* For file-based config: reads from disk
|
|
431
|
+
* For data config: uses pre-loaded data directly
|
|
432
|
+
*/
|
|
433
|
+
async initialize() {
|
|
434
|
+
if (this.initialized) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
if (isDataConfig(this.config)) {
|
|
439
|
+
this.docs = this.config.docs;
|
|
440
|
+
this.searchIndex = await importSearchIndex(this.config.searchIndexData);
|
|
441
|
+
} else if (isFileConfig(this.config)) {
|
|
442
|
+
if (await fs2.pathExists(this.config.docsPath)) {
|
|
443
|
+
this.docs = await fs2.readJson(this.config.docsPath);
|
|
444
|
+
} else {
|
|
445
|
+
throw new Error(`Docs file not found: ${this.config.docsPath}`);
|
|
446
|
+
}
|
|
447
|
+
if (await fs2.pathExists(this.config.indexPath)) {
|
|
448
|
+
const indexData = await fs2.readJson(this.config.indexPath);
|
|
449
|
+
this.searchIndex = await importSearchIndex(indexData);
|
|
450
|
+
} else {
|
|
451
|
+
throw new Error(`Search index not found: ${this.config.indexPath}`);
|
|
452
|
+
}
|
|
453
|
+
} else {
|
|
454
|
+
throw new Error("Invalid server config: must provide either file paths or pre-loaded data");
|
|
455
|
+
}
|
|
456
|
+
this.initialized = true;
|
|
457
|
+
} catch (error) {
|
|
458
|
+
console.error("[MCP] Failed to initialize:", error);
|
|
459
|
+
throw error;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Handle an HTTP request using the MCP SDK's transport
|
|
464
|
+
*
|
|
465
|
+
* This method is designed for serverless environments (Vercel, Netlify).
|
|
466
|
+
* It creates a stateless transport instance and processes the request.
|
|
467
|
+
*
|
|
468
|
+
* @param req - Node.js IncomingMessage or compatible request object
|
|
469
|
+
* @param res - Node.js ServerResponse or compatible response object
|
|
470
|
+
* @param parsedBody - Optional pre-parsed request body
|
|
471
|
+
*/
|
|
472
|
+
async handleHttpRequest(req, res, parsedBody) {
|
|
473
|
+
await this.initialize();
|
|
474
|
+
const transport = new StreamableHTTPServerTransport({
|
|
475
|
+
sessionIdGenerator: void 0,
|
|
476
|
+
// Stateless mode - no session tracking
|
|
477
|
+
enableJsonResponse: true
|
|
478
|
+
// Return JSON instead of SSE streams
|
|
479
|
+
});
|
|
480
|
+
await this.mcpServer.connect(transport);
|
|
481
|
+
try {
|
|
482
|
+
await transport.handleRequest(req, res, parsedBody);
|
|
483
|
+
} finally {
|
|
484
|
+
await transport.close();
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Handle a Web Standard Request (Cloudflare Workers, Deno, Bun)
|
|
489
|
+
*
|
|
490
|
+
* This method is designed for Web Standard environments that use
|
|
491
|
+
* the Fetch API Request/Response pattern.
|
|
492
|
+
*
|
|
493
|
+
* @param request - Web Standard Request object
|
|
494
|
+
* @returns Web Standard Response object
|
|
495
|
+
*/
|
|
496
|
+
async handleWebRequest(request) {
|
|
497
|
+
await this.initialize();
|
|
498
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
499
|
+
sessionIdGenerator: void 0,
|
|
500
|
+
// Stateless mode
|
|
501
|
+
enableJsonResponse: true
|
|
502
|
+
});
|
|
503
|
+
await this.mcpServer.connect(transport);
|
|
504
|
+
try {
|
|
505
|
+
return await transport.handleRequest(request);
|
|
506
|
+
} finally {
|
|
507
|
+
await transport.close();
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Get server status information
|
|
512
|
+
*
|
|
513
|
+
* Useful for health checks and debugging
|
|
514
|
+
*/
|
|
515
|
+
async getStatus() {
|
|
516
|
+
return {
|
|
517
|
+
name: this.config.name,
|
|
518
|
+
version: this.config.version ?? "1.0.0",
|
|
519
|
+
initialized: this.initialized,
|
|
520
|
+
docCount: this.docs ? Object.keys(this.docs).length : 0,
|
|
521
|
+
baseUrl: this.config.baseUrl
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Get the underlying McpServer instance
|
|
526
|
+
*
|
|
527
|
+
* Useful for advanced use cases like custom transports
|
|
528
|
+
*/
|
|
529
|
+
getMcpServer() {
|
|
530
|
+
return this.mcpServer;
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
// src/cli/verify.ts
|
|
535
|
+
async function verifyBuild(buildDir) {
|
|
536
|
+
const result = {
|
|
537
|
+
success: true,
|
|
538
|
+
docsFound: 0,
|
|
539
|
+
errors: [],
|
|
540
|
+
warnings: []
|
|
541
|
+
};
|
|
542
|
+
const mcpDir = path.join(buildDir, "mcp");
|
|
543
|
+
if (!await fs2.pathExists(mcpDir)) {
|
|
544
|
+
result.errors.push(`MCP directory not found: ${mcpDir}`);
|
|
545
|
+
result.errors.push('Did you run "npm run build" with the MCP plugin configured?');
|
|
546
|
+
result.success = false;
|
|
547
|
+
return result;
|
|
548
|
+
}
|
|
549
|
+
const requiredFiles = ["docs.json", "search-index.json", "manifest.json"];
|
|
550
|
+
for (const file of requiredFiles) {
|
|
551
|
+
const filePath = path.join(mcpDir, file);
|
|
552
|
+
if (!await fs2.pathExists(filePath)) {
|
|
553
|
+
result.errors.push(`Required file missing: ${filePath}`);
|
|
554
|
+
result.success = false;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (!result.success) {
|
|
558
|
+
return result;
|
|
559
|
+
}
|
|
560
|
+
try {
|
|
561
|
+
const docsPath = path.join(mcpDir, "docs.json");
|
|
562
|
+
const docs = await fs2.readJson(docsPath);
|
|
563
|
+
if (typeof docs !== "object" || docs === null) {
|
|
564
|
+
result.errors.push("docs.json is not a valid object");
|
|
565
|
+
result.success = false;
|
|
566
|
+
} else {
|
|
567
|
+
result.docsFound = Object.keys(docs).length;
|
|
568
|
+
if (result.docsFound === 0) {
|
|
569
|
+
result.warnings.push("docs.json contains no documents");
|
|
570
|
+
}
|
|
571
|
+
for (const [route, doc] of Object.entries(docs)) {
|
|
572
|
+
const d = doc;
|
|
573
|
+
if (!d.title || typeof d.title !== "string") {
|
|
574
|
+
result.warnings.push(`Document ${route} is missing a title`);
|
|
575
|
+
}
|
|
576
|
+
if (!d.markdown || typeof d.markdown !== "string") {
|
|
577
|
+
result.warnings.push(`Document ${route} is missing markdown content`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
} catch (error) {
|
|
582
|
+
result.errors.push(`Failed to parse docs.json: ${error.message}`);
|
|
583
|
+
result.success = false;
|
|
584
|
+
}
|
|
585
|
+
try {
|
|
586
|
+
const indexPath = path.join(mcpDir, "search-index.json");
|
|
587
|
+
const indexData = await fs2.readJson(indexPath);
|
|
588
|
+
if (typeof indexData !== "object" || indexData === null) {
|
|
589
|
+
result.errors.push("search-index.json is not a valid object");
|
|
590
|
+
result.success = false;
|
|
591
|
+
}
|
|
592
|
+
} catch (error) {
|
|
593
|
+
result.errors.push(`Failed to parse search-index.json: ${error.message}`);
|
|
594
|
+
result.success = false;
|
|
595
|
+
}
|
|
596
|
+
try {
|
|
597
|
+
const manifestPath = path.join(mcpDir, "manifest.json");
|
|
598
|
+
const manifest = await fs2.readJson(manifestPath);
|
|
599
|
+
if (!manifest.name || typeof manifest.name !== "string") {
|
|
600
|
+
result.warnings.push("manifest.json is missing server name");
|
|
601
|
+
}
|
|
602
|
+
if (!manifest.version || typeof manifest.version !== "string") {
|
|
603
|
+
result.warnings.push("manifest.json is missing server version");
|
|
604
|
+
}
|
|
605
|
+
} catch (error) {
|
|
606
|
+
result.errors.push(`Failed to parse manifest.json: ${error.message}`);
|
|
607
|
+
result.success = false;
|
|
608
|
+
}
|
|
609
|
+
return result;
|
|
610
|
+
}
|
|
611
|
+
async function testServer(buildDir) {
|
|
612
|
+
const mcpDir = path.join(buildDir, "mcp");
|
|
613
|
+
const docsPath = path.join(mcpDir, "docs.json");
|
|
614
|
+
const indexPath = path.join(mcpDir, "search-index.json");
|
|
615
|
+
const manifestPath = path.join(mcpDir, "manifest.json");
|
|
616
|
+
try {
|
|
617
|
+
const manifest = await fs2.readJson(manifestPath);
|
|
618
|
+
const docs = await fs2.readJson(docsPath);
|
|
619
|
+
const searchIndexData = await fs2.readJson(indexPath);
|
|
620
|
+
const server = new McpDocsServer({
|
|
621
|
+
name: manifest.name || "test-docs",
|
|
622
|
+
version: manifest.version || "1.0.0",
|
|
623
|
+
docs,
|
|
624
|
+
searchIndexData
|
|
625
|
+
});
|
|
626
|
+
await server.initialize();
|
|
627
|
+
const status = await server.getStatus();
|
|
628
|
+
if (!status.initialized) {
|
|
629
|
+
return { success: false, message: "Server failed to initialize" };
|
|
630
|
+
}
|
|
631
|
+
if (status.docCount === 0) {
|
|
632
|
+
return { success: false, message: "Server has no documents loaded" };
|
|
633
|
+
}
|
|
634
|
+
return {
|
|
635
|
+
success: true,
|
|
636
|
+
message: `Server initialized with ${status.docCount} documents`
|
|
637
|
+
};
|
|
638
|
+
} catch (error) {
|
|
639
|
+
return {
|
|
640
|
+
success: false,
|
|
641
|
+
message: `Server test failed: ${error.message}`
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
async function main() {
|
|
646
|
+
const args = process.argv.slice(2);
|
|
647
|
+
const buildDir = args[0] || "./build";
|
|
648
|
+
console.log("");
|
|
649
|
+
console.log("\u{1F50D} MCP Build Verification");
|
|
650
|
+
console.log("=".repeat(50));
|
|
651
|
+
console.log(`Build directory: ${path.resolve(buildDir)}`);
|
|
652
|
+
console.log("");
|
|
653
|
+
console.log("\u{1F4C1} Checking build output...");
|
|
654
|
+
const verifyResult = await verifyBuild(buildDir);
|
|
655
|
+
if (verifyResult.errors.length > 0) {
|
|
656
|
+
console.log("");
|
|
657
|
+
console.log("\u274C Errors:");
|
|
658
|
+
for (const error of verifyResult.errors) {
|
|
659
|
+
console.log(` \u2022 ${error}`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (verifyResult.warnings.length > 0) {
|
|
663
|
+
console.log("");
|
|
664
|
+
console.log("\u26A0\uFE0F Warnings:");
|
|
665
|
+
for (const warning of verifyResult.warnings) {
|
|
666
|
+
console.log(` \u2022 ${warning}`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (!verifyResult.success) {
|
|
670
|
+
console.log("");
|
|
671
|
+
console.log("\u274C Build verification failed");
|
|
672
|
+
process.exit(1);
|
|
673
|
+
}
|
|
674
|
+
console.log(` \u2713 Found ${verifyResult.docsFound} documents`);
|
|
675
|
+
console.log(" \u2713 All required files present");
|
|
676
|
+
console.log(" \u2713 File structure valid");
|
|
677
|
+
console.log("");
|
|
678
|
+
console.log("\u{1F680} Testing MCP server...");
|
|
679
|
+
const serverResult = await testServer(buildDir);
|
|
680
|
+
if (!serverResult.success) {
|
|
681
|
+
console.log(` \u274C ${serverResult.message}`);
|
|
682
|
+
console.log("");
|
|
683
|
+
console.log("\u274C Server test failed");
|
|
684
|
+
process.exit(1);
|
|
685
|
+
}
|
|
686
|
+
console.log(` \u2713 ${serverResult.message}`);
|
|
687
|
+
console.log("");
|
|
688
|
+
console.log("\u2705 All checks passed!");
|
|
689
|
+
console.log("");
|
|
690
|
+
console.log("Next steps:");
|
|
691
|
+
console.log(" 1. Deploy your site to a hosting provider");
|
|
692
|
+
console.log(" 2. Configure MCP endpoint (see README for platform guides)");
|
|
693
|
+
console.log(" 3. Connect your AI tools to the MCP server");
|
|
694
|
+
console.log("");
|
|
695
|
+
process.exit(0);
|
|
696
|
+
}
|
|
697
|
+
main().catch((error) => {
|
|
698
|
+
console.error("Unexpected error:", error);
|
|
699
|
+
process.exit(1);
|
|
700
|
+
});
|
|
701
|
+
//# sourceMappingURL=verify.mjs.map
|
|
702
|
+
//# sourceMappingURL=verify.mjs.map
|