@farming-labs/docs 0.1.1-beta.2 → 0.1.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/dist/api-reference-wh4_pwG8.mjs +802 -0
- package/dist/cli/index.d.mts +8 -1
- package/dist/cli/index.mjs +117 -8
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -0
- package/dist/mcp.d.mts +86 -0
- package/dist/mcp.mjs +485 -0
- package/dist/server.d.mts +3 -2
- package/dist/server.mjs +3 -801
- package/dist/{types-DpijZMth.d.mts → types-Bd3kyFF1.d.mts} +52 -1
- package/package.json +10 -3
package/dist/mcp.mjs
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import matter from "gray-matter";
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio";
|
|
7
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp";
|
|
8
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types";
|
|
9
|
+
import * as z from "zod/v4";
|
|
10
|
+
|
|
11
|
+
//#region src/mcp.ts
|
|
12
|
+
const DEFAULT_MCP_ROUTE = "/api/docs/mcp";
|
|
13
|
+
const DEFAULT_MCP_VERSION = "0.0.0";
|
|
14
|
+
const DEFAULT_MCP_NAME = "@farming-labs/docs";
|
|
15
|
+
const searchDocsInputSchema = z.object({
|
|
16
|
+
query: z.string().trim().min(1),
|
|
17
|
+
limit: z.number().int().min(1).max(25).optional(),
|
|
18
|
+
locale: z.string().min(1).optional()
|
|
19
|
+
});
|
|
20
|
+
const readPageInputSchema = z.object({
|
|
21
|
+
path: z.string().min(1),
|
|
22
|
+
locale: z.string().min(1).optional()
|
|
23
|
+
});
|
|
24
|
+
const listPagesInputSchema = z.object({ locale: z.string().min(1).optional() });
|
|
25
|
+
const getNavigationInputSchema = z.object({ locale: z.string().min(1).optional() });
|
|
26
|
+
function normalizeDocsMcpRoute(route) {
|
|
27
|
+
if (!route || route.trim().length === 0) return DEFAULT_MCP_ROUTE;
|
|
28
|
+
const normalized = `/${route}`.replace(/\/+/g, "/");
|
|
29
|
+
return normalized !== "/" ? normalized.replace(/\/+$/, "") : DEFAULT_MCP_ROUTE;
|
|
30
|
+
}
|
|
31
|
+
function resolveDocsMcpConfig(mcp, defaults = {}) {
|
|
32
|
+
if (!mcp) return {
|
|
33
|
+
enabled: false,
|
|
34
|
+
route: normalizeDocsMcpRoute(defaults.defaultRoute),
|
|
35
|
+
name: defaults.defaultName ?? DEFAULT_MCP_NAME,
|
|
36
|
+
version: defaults.defaultVersion ?? DEFAULT_MCP_VERSION,
|
|
37
|
+
tools: {
|
|
38
|
+
listPages: true,
|
|
39
|
+
readPage: true,
|
|
40
|
+
searchDocs: true,
|
|
41
|
+
getNavigation: true
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const config = typeof mcp === "object" ? mcp : {};
|
|
45
|
+
return {
|
|
46
|
+
enabled: typeof mcp === "boolean" ? mcp : config.enabled ?? true,
|
|
47
|
+
route: normalizeDocsMcpRoute(config.route ?? defaults.defaultRoute),
|
|
48
|
+
name: config.name ?? defaults.defaultName ?? DEFAULT_MCP_NAME,
|
|
49
|
+
version: config.version ?? defaults.defaultVersion ?? DEFAULT_MCP_VERSION,
|
|
50
|
+
tools: {
|
|
51
|
+
listPages: config.tools?.listPages ?? true,
|
|
52
|
+
readPage: config.tools?.readPage ?? true,
|
|
53
|
+
searchDocs: config.tools?.searchDocs ?? true,
|
|
54
|
+
getNavigation: config.tools?.getNavigation ?? true
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function createFilesystemDocsMcpSource(options = {}) {
|
|
59
|
+
const rootDir = options.rootDir ?? process.cwd();
|
|
60
|
+
const entry = normalizePathSegment(options.entry ?? "docs") || "docs";
|
|
61
|
+
const contentDir = options.contentDir ?? entry;
|
|
62
|
+
const contentDirAbs = path.resolve(rootDir, contentDir);
|
|
63
|
+
const cache = /* @__PURE__ */ new Map();
|
|
64
|
+
const navigationCache = /* @__PURE__ */ new Map();
|
|
65
|
+
function getPages() {
|
|
66
|
+
const cached = cache.get("__default__");
|
|
67
|
+
if (cached) return cached;
|
|
68
|
+
const pages = scanFilesystemDocsPages(contentDirAbs, entry);
|
|
69
|
+
cache.set("__default__", pages);
|
|
70
|
+
return pages;
|
|
71
|
+
}
|
|
72
|
+
function getNavigation() {
|
|
73
|
+
const cached = navigationCache.get("__default__");
|
|
74
|
+
if (cached) return cached;
|
|
75
|
+
const tree = buildNavigationTreeFromPages(getPages(), options.siteTitle ?? "Documentation", options.ordering);
|
|
76
|
+
navigationCache.set("__default__", tree);
|
|
77
|
+
return tree;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
entry,
|
|
81
|
+
siteTitle: options.siteTitle ?? "Documentation",
|
|
82
|
+
getPages,
|
|
83
|
+
getNavigation
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function createDocsMcpServer(options) {
|
|
87
|
+
const resolved = resolveDocsMcpConfig(options.mcp, {
|
|
88
|
+
defaultName: options.defaultName ?? options.source.siteTitle ?? DEFAULT_MCP_NAME,
|
|
89
|
+
defaultVersion: options.defaultVersion
|
|
90
|
+
});
|
|
91
|
+
const server = new McpServer({
|
|
92
|
+
name: resolved.name,
|
|
93
|
+
version: resolved.version
|
|
94
|
+
});
|
|
95
|
+
const defaultPages = dedupePages(await options.source.getPages());
|
|
96
|
+
const defaultTree = await options.source.getNavigation();
|
|
97
|
+
server.registerResource("docs-navigation", "docs://navigation", {
|
|
98
|
+
title: "Docs Navigation",
|
|
99
|
+
description: "Structured navigation tree for the documentation site.",
|
|
100
|
+
mimeType: "text/plain"
|
|
101
|
+
}, async () => ({ contents: [{
|
|
102
|
+
uri: "docs://navigation",
|
|
103
|
+
mimeType: "text/plain",
|
|
104
|
+
text: renderNavigationTree(defaultTree)
|
|
105
|
+
}] }));
|
|
106
|
+
for (const page of defaultPages) {
|
|
107
|
+
const resourceUri = toPageResourceUri(page.url);
|
|
108
|
+
server.registerResource(`page-${slugToKey(page.slug)}`, resourceUri, {
|
|
109
|
+
title: page.title,
|
|
110
|
+
description: page.description,
|
|
111
|
+
mimeType: "text/markdown"
|
|
112
|
+
}, async () => ({ contents: [{
|
|
113
|
+
uri: resourceUri,
|
|
114
|
+
mimeType: "text/markdown",
|
|
115
|
+
text: renderPageDocument(page)
|
|
116
|
+
}] }));
|
|
117
|
+
}
|
|
118
|
+
if (resolved.tools.listPages) server.registerTool("list_pages", {
|
|
119
|
+
title: "List docs pages",
|
|
120
|
+
description: "List the known documentation pages with titles, slugs, and URLs.",
|
|
121
|
+
inputSchema: listPagesInputSchema,
|
|
122
|
+
annotations: { readOnlyHint: true }
|
|
123
|
+
}, async ({ locale }) => {
|
|
124
|
+
const pages = toPageSummaries(dedupePages(await options.source.getPages(locale)));
|
|
125
|
+
return { content: [{
|
|
126
|
+
type: "text",
|
|
127
|
+
text: JSON.stringify({ pages }, null, 2)
|
|
128
|
+
}] };
|
|
129
|
+
});
|
|
130
|
+
if (resolved.tools.getNavigation) server.registerTool("get_navigation", {
|
|
131
|
+
title: "Get docs navigation",
|
|
132
|
+
description: "Return the documentation navigation tree for the current docs site.",
|
|
133
|
+
inputSchema: getNavigationInputSchema,
|
|
134
|
+
annotations: { readOnlyHint: true }
|
|
135
|
+
}, async ({ locale }) => {
|
|
136
|
+
return { content: [{
|
|
137
|
+
type: "text",
|
|
138
|
+
text: renderNavigationTree(await options.source.getNavigation(locale))
|
|
139
|
+
}] };
|
|
140
|
+
});
|
|
141
|
+
if (resolved.tools.searchDocs) server.registerTool("search_docs", {
|
|
142
|
+
title: "Search documentation",
|
|
143
|
+
description: "Search the docs by keyword across titles, descriptions, and page content.",
|
|
144
|
+
inputSchema: searchDocsInputSchema,
|
|
145
|
+
annotations: { readOnlyHint: true }
|
|
146
|
+
}, async ({ query, limit, locale }) => {
|
|
147
|
+
const results = searchDocsPages(dedupePages(await options.source.getPages(locale)), query, limit ?? 10);
|
|
148
|
+
return { content: [{
|
|
149
|
+
type: "text",
|
|
150
|
+
text: JSON.stringify({ results }, null, 2)
|
|
151
|
+
}] };
|
|
152
|
+
});
|
|
153
|
+
if (resolved.tools.readPage) server.registerTool("read_page", {
|
|
154
|
+
title: "Read a docs page",
|
|
155
|
+
description: "Read a documentation page by slug or URL path.",
|
|
156
|
+
inputSchema: readPageInputSchema,
|
|
157
|
+
annotations: { readOnlyHint: true }
|
|
158
|
+
}, async ({ path: requestedPath, locale }) => {
|
|
159
|
+
const page = findDocsPage(dedupePages(await options.source.getPages(locale)), requestedPath, options.source.entry);
|
|
160
|
+
if (!page) return {
|
|
161
|
+
content: [{
|
|
162
|
+
type: "text",
|
|
163
|
+
text: JSON.stringify({ error: `No docs page matched "${requestedPath}".` }, null, 2)
|
|
164
|
+
}],
|
|
165
|
+
isError: true
|
|
166
|
+
};
|
|
167
|
+
return { content: [{
|
|
168
|
+
type: "text",
|
|
169
|
+
text: renderPageDocument(page)
|
|
170
|
+
}] };
|
|
171
|
+
});
|
|
172
|
+
return server;
|
|
173
|
+
}
|
|
174
|
+
function createDocsMcpHttpHandler(options) {
|
|
175
|
+
if (!resolveDocsMcpConfig(options.mcp, {
|
|
176
|
+
defaultName: options.defaultName ?? options.source.siteTitle ?? DEFAULT_MCP_NAME,
|
|
177
|
+
defaultVersion: options.defaultVersion
|
|
178
|
+
}).enabled) return {
|
|
179
|
+
GET: async () => createJsonErrorResponse(404, "MCP is not enabled. Set `mcp: { enabled: true }` in docs.config to enable it."),
|
|
180
|
+
POST: async () => createJsonErrorResponse(404, "MCP is not enabled. Set `mcp: { enabled: true }` in docs.config to enable it."),
|
|
181
|
+
DELETE: async () => createJsonErrorResponse(404, "MCP is not enabled. Set `mcp: { enabled: true }` in docs.config to enable it.")
|
|
182
|
+
};
|
|
183
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
184
|
+
async function createSession() {
|
|
185
|
+
const server = await createDocsMcpServer(options);
|
|
186
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
187
|
+
sessionIdGenerator: () => randomUUID(),
|
|
188
|
+
onsessioninitialized(sessionId) {
|
|
189
|
+
sessions.set(sessionId, {
|
|
190
|
+
server,
|
|
191
|
+
transport
|
|
192
|
+
});
|
|
193
|
+
},
|
|
194
|
+
async onsessionclosed(sessionId) {
|
|
195
|
+
const session = sessions.get(sessionId);
|
|
196
|
+
sessions.delete(sessionId);
|
|
197
|
+
await session?.server.close().catch(() => void 0);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
await server.connect(transport);
|
|
201
|
+
return {
|
|
202
|
+
server,
|
|
203
|
+
transport
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
async function handle(request) {
|
|
207
|
+
const method = request.method.toUpperCase();
|
|
208
|
+
const sessionId = request.headers.get("mcp-session-id") ?? request.headers.get("Mcp-Session-Id");
|
|
209
|
+
const existing = sessionId ? sessions.get(sessionId) : void 0;
|
|
210
|
+
let parsedBody;
|
|
211
|
+
if (method === "POST") try {
|
|
212
|
+
parsedBody = await request.clone().json();
|
|
213
|
+
} catch {
|
|
214
|
+
parsedBody = void 0;
|
|
215
|
+
}
|
|
216
|
+
const initializeRequest = method === "POST" && parsedBody && isInitializeRequest(parsedBody);
|
|
217
|
+
if (!existing) {
|
|
218
|
+
if (!initializeRequest) return createJsonErrorResponse(method === "DELETE" ? 404 : 400, "MCP session not initialized. Start with an initialize request against this endpoint.");
|
|
219
|
+
return (await createSession()).transport.handleRequest(request, parsedBody === void 0 ? void 0 : { parsedBody });
|
|
220
|
+
}
|
|
221
|
+
return existing.transport.handleRequest(request, parsedBody === void 0 ? void 0 : { parsedBody });
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
GET: async ({ request }) => handle(request),
|
|
225
|
+
POST: async ({ request }) => handle(request),
|
|
226
|
+
DELETE: async ({ request }) => handle(request)
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
async function runDocsMcpStdio(options) {
|
|
230
|
+
const server = await createDocsMcpServer(options);
|
|
231
|
+
const transport = new StdioServerTransport();
|
|
232
|
+
await server.connect(transport);
|
|
233
|
+
}
|
|
234
|
+
function createJsonErrorResponse(status, error) {
|
|
235
|
+
return new Response(JSON.stringify({ error }), {
|
|
236
|
+
status,
|
|
237
|
+
headers: { "Content-Type": "application/json" }
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function normalizePathSegment(value) {
|
|
241
|
+
return value.replace(/^\/+|\/+$/g, "");
|
|
242
|
+
}
|
|
243
|
+
function titleize(value) {
|
|
244
|
+
return value.replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
245
|
+
}
|
|
246
|
+
function stripMarkdownForMcp(content) {
|
|
247
|
+
return content.replace(/^(import|export)\s.*$/gm, "").replace(/<[^>]+\/>/g, "").replace(/<\/?[A-Z][^>]*>/g, "").replace(/<\/?[a-z][^>]*>/g, "").replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/^#{1,6}\s+/gm, "").replace(/(\*{1,3}|_{1,3})(.*?)\1/g, "$2").replace(/```[\s\S]*?```/g, "").replace(/`([^`]+)`/g, "$1").replace(/^>\s+/gm, "").replace(/^[-*_]{3,}\s*$/gm, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
248
|
+
}
|
|
249
|
+
function scanFilesystemDocsPages(contentDirAbs, entry) {
|
|
250
|
+
const pages = [];
|
|
251
|
+
function scan(dir, slugParts) {
|
|
252
|
+
if (!fs.existsSync(dir)) return;
|
|
253
|
+
const entries = fs.readdirSync(dir).sort();
|
|
254
|
+
for (const name of entries) {
|
|
255
|
+
const full = path.join(dir, name);
|
|
256
|
+
if (fs.statSync(full).isDirectory()) {
|
|
257
|
+
scan(full, [...slugParts, name]);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (!name.endsWith(".md") && !name.endsWith(".mdx") && !name.endsWith(".svx")) continue;
|
|
261
|
+
const { data, content } = matter(fs.readFileSync(full, "utf-8"));
|
|
262
|
+
const baseName = name.replace(/\.(md|mdx|svx)$/, "");
|
|
263
|
+
const isIndex = baseName === "index" || baseName === "page" || baseName === "+page";
|
|
264
|
+
const slug = isIndex ? slugParts.join("/") : [...slugParts, baseName].join("/");
|
|
265
|
+
const url = slug ? `/${entry}/${slug}` : `/${entry}`;
|
|
266
|
+
const title = data.title ?? (isIndex ? slugParts.length > 0 ? titleize(slugParts[slugParts.length - 1]) : "Documentation" : titleize(baseName));
|
|
267
|
+
pages.push({
|
|
268
|
+
slug,
|
|
269
|
+
url,
|
|
270
|
+
title,
|
|
271
|
+
description: data.description,
|
|
272
|
+
icon: data.icon,
|
|
273
|
+
content: stripMarkdownForMcp(content),
|
|
274
|
+
rawContent: content,
|
|
275
|
+
order: typeof data.order === "number" ? data.order : Number.POSITIVE_INFINITY
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
scan(contentDirAbs, []);
|
|
280
|
+
return pages;
|
|
281
|
+
}
|
|
282
|
+
function buildNavigationTreeFromPages(pages, siteTitle, ordering) {
|
|
283
|
+
const bySlug = new Map(pages.map((page) => [page.slug, page]));
|
|
284
|
+
const rootPage = bySlug.get("");
|
|
285
|
+
function childOrderFor(parentSlug) {
|
|
286
|
+
if (!Array.isArray(ordering)) return void 0;
|
|
287
|
+
if (!parentSlug) return ordering;
|
|
288
|
+
let items = ordering;
|
|
289
|
+
for (const segment of parentSlug.split("/")) {
|
|
290
|
+
items = (items?.find((item) => item.slug === segment))?.children;
|
|
291
|
+
if (!items) return void 0;
|
|
292
|
+
}
|
|
293
|
+
return items;
|
|
294
|
+
}
|
|
295
|
+
function sortChildSlugs(childSlugs, parentSlug) {
|
|
296
|
+
const explicitOrder = childOrderFor(parentSlug);
|
|
297
|
+
if (explicitOrder) {
|
|
298
|
+
const explicit = new Set(explicitOrder.map((item) => item.slug));
|
|
299
|
+
const ordered = [];
|
|
300
|
+
for (const item of explicitOrder) {
|
|
301
|
+
const childSlug = parentSlug ? `${parentSlug}/${item.slug}` : item.slug;
|
|
302
|
+
if (childSlugs.includes(childSlug)) ordered.push(childSlug);
|
|
303
|
+
}
|
|
304
|
+
for (const childSlug of childSlugs) {
|
|
305
|
+
const segment = childSlug.split("/").pop() ?? childSlug;
|
|
306
|
+
if (!explicit.has(segment)) ordered.push(childSlug);
|
|
307
|
+
}
|
|
308
|
+
return ordered;
|
|
309
|
+
}
|
|
310
|
+
if (ordering === "numeric") return [...childSlugs].sort((left, right) => {
|
|
311
|
+
const leftPage = bySlug.get(left);
|
|
312
|
+
const rightPage = bySlug.get(right);
|
|
313
|
+
const leftOrder = leftPage?.order ?? Number.POSITIVE_INFINITY;
|
|
314
|
+
const rightOrder = rightPage?.order ?? Number.POSITIVE_INFINITY;
|
|
315
|
+
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
|
|
316
|
+
return left.localeCompare(right);
|
|
317
|
+
});
|
|
318
|
+
return [...childSlugs].sort((left, right) => left.localeCompare(right));
|
|
319
|
+
}
|
|
320
|
+
function buildLevel(parentSlug) {
|
|
321
|
+
const prefix = parentSlug ? `${parentSlug}/` : "";
|
|
322
|
+
const childSet = /* @__PURE__ */ new Set();
|
|
323
|
+
for (const page of pages) {
|
|
324
|
+
if (!page.slug.startsWith(prefix) || page.slug === parentSlug) continue;
|
|
325
|
+
const remainder = page.slug.slice(prefix.length);
|
|
326
|
+
if (!remainder) continue;
|
|
327
|
+
const [firstSegment] = remainder.split("/");
|
|
328
|
+
childSet.add(parentSlug ? `${parentSlug}/${firstSegment}` : firstSegment);
|
|
329
|
+
}
|
|
330
|
+
const childSlugs = sortChildSlugs([...childSet], parentSlug);
|
|
331
|
+
const nodes = [];
|
|
332
|
+
for (const childSlug of childSlugs) {
|
|
333
|
+
const page = bySlug.get(childSlug);
|
|
334
|
+
const hasChildren = pages.some((candidate) => candidate.slug.startsWith(`${childSlug}/`));
|
|
335
|
+
const segment = childSlug.split("/").pop() ?? childSlug;
|
|
336
|
+
const name = page?.title ?? titleize(segment);
|
|
337
|
+
const icon = page?.icon;
|
|
338
|
+
const description = page?.description;
|
|
339
|
+
if (hasChildren) {
|
|
340
|
+
nodes.push({
|
|
341
|
+
type: "folder",
|
|
342
|
+
name,
|
|
343
|
+
icon,
|
|
344
|
+
index: page ? {
|
|
345
|
+
type: "page",
|
|
346
|
+
name: page.title,
|
|
347
|
+
url: page.url,
|
|
348
|
+
icon: page.icon,
|
|
349
|
+
description: page.description
|
|
350
|
+
} : void 0,
|
|
351
|
+
children: buildLevel(childSlug)
|
|
352
|
+
});
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (!page) continue;
|
|
356
|
+
nodes.push({
|
|
357
|
+
type: "page",
|
|
358
|
+
name,
|
|
359
|
+
url: page.url,
|
|
360
|
+
icon,
|
|
361
|
+
description
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
return nodes;
|
|
365
|
+
}
|
|
366
|
+
const children = [];
|
|
367
|
+
if (rootPage) children.push({
|
|
368
|
+
type: "page",
|
|
369
|
+
name: rootPage.title,
|
|
370
|
+
url: rootPage.url,
|
|
371
|
+
icon: rootPage.icon,
|
|
372
|
+
description: rootPage.description
|
|
373
|
+
});
|
|
374
|
+
children.push(...buildLevel(""));
|
|
375
|
+
return {
|
|
376
|
+
name: siteTitle,
|
|
377
|
+
children
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function dedupePages(pages) {
|
|
381
|
+
const seen = /* @__PURE__ */ new Map();
|
|
382
|
+
for (const page of pages) seen.set(page.url, page);
|
|
383
|
+
return [...seen.values()];
|
|
384
|
+
}
|
|
385
|
+
function toPageSummaries(pages) {
|
|
386
|
+
return pages.map((page) => ({
|
|
387
|
+
slug: page.slug,
|
|
388
|
+
url: page.url,
|
|
389
|
+
title: page.title,
|
|
390
|
+
description: page.description,
|
|
391
|
+
icon: page.icon
|
|
392
|
+
}));
|
|
393
|
+
}
|
|
394
|
+
function findDocsPage(pages, requestedPath, entry) {
|
|
395
|
+
const normalizedRequest = normalizeRequestedPath(requestedPath, entry);
|
|
396
|
+
for (const page of pages) if (normalizeUrlPath(page.url) === normalizedRequest) return page;
|
|
397
|
+
const normalizedSlug = normalizePathSegment(requestedPath.replace(/^\//, ""));
|
|
398
|
+
for (const page of pages) if (normalizePathSegment(page.slug) === normalizedSlug) return page;
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
function normalizeRequestedPath(requestedPath, entry) {
|
|
402
|
+
const trimmed = requestedPath.trim();
|
|
403
|
+
if (!trimmed) return "/";
|
|
404
|
+
if (/^https?:\/\//i.test(trimmed)) try {
|
|
405
|
+
return normalizeUrlPath(new URL(trimmed).pathname);
|
|
406
|
+
} catch {
|
|
407
|
+
return "/";
|
|
408
|
+
}
|
|
409
|
+
const normalized = normalizeUrlPath(trimmed.startsWith("/") ? trimmed : `/${trimmed}`);
|
|
410
|
+
if (!entry) return normalized;
|
|
411
|
+
const normalizedEntry = `/${normalizePathSegment(entry)}`;
|
|
412
|
+
if (normalized === normalizedEntry || normalized.startsWith(`${normalizedEntry}/`)) return normalized;
|
|
413
|
+
const slug = normalizePathSegment(trimmed);
|
|
414
|
+
return slug ? normalizeUrlPath(`${normalizedEntry}/${slug}`) : normalizedEntry;
|
|
415
|
+
}
|
|
416
|
+
function normalizeUrlPath(value) {
|
|
417
|
+
const normalized = value.replace(/\/+/g, "/");
|
|
418
|
+
if (normalized === "/") return normalized;
|
|
419
|
+
return normalized.replace(/\/+$/, "");
|
|
420
|
+
}
|
|
421
|
+
function searchDocsPages(pages, query, limit) {
|
|
422
|
+
const normalizedQuery = query.toLowerCase().trim();
|
|
423
|
+
if (!normalizedQuery) return [];
|
|
424
|
+
const words = normalizedQuery.split(/\s+/).filter(Boolean);
|
|
425
|
+
return pages.map((page) => {
|
|
426
|
+
const titleScore = page.title.toLowerCase().includes(normalizedQuery) ? 10 : 0;
|
|
427
|
+
const descriptionScore = page.description?.toLowerCase().includes(normalizedQuery) ? 4 : 0;
|
|
428
|
+
const contentScore = words.reduce((score, word) => {
|
|
429
|
+
return score + (page.content.toLowerCase().includes(word) ? 1 : 0);
|
|
430
|
+
}, 0);
|
|
431
|
+
return {
|
|
432
|
+
slug: page.slug,
|
|
433
|
+
url: page.url,
|
|
434
|
+
title: page.title,
|
|
435
|
+
description: page.description,
|
|
436
|
+
icon: page.icon,
|
|
437
|
+
excerpt: buildExcerpt(page, words),
|
|
438
|
+
score: titleScore + descriptionScore + contentScore
|
|
439
|
+
};
|
|
440
|
+
}).filter((page) => page.score > 0).sort((left, right) => right.score - left.score).slice(0, limit).map(({ score: _score, ...page }) => page);
|
|
441
|
+
}
|
|
442
|
+
function buildExcerpt(page, words) {
|
|
443
|
+
const haystack = page.rawContent ?? page.content;
|
|
444
|
+
const lower = haystack.toLowerCase();
|
|
445
|
+
const firstHit = words.find((word) => lower.includes(word.toLowerCase()));
|
|
446
|
+
if (!firstHit) return page.description;
|
|
447
|
+
const index = lower.indexOf(firstHit.toLowerCase());
|
|
448
|
+
const start = Math.max(0, index - 80);
|
|
449
|
+
const end = Math.min(haystack.length, index + 140);
|
|
450
|
+
const excerpt = haystack.slice(start, end).replace(/\s+/g, " ").trim();
|
|
451
|
+
return excerpt.length > 0 ? excerpt : page.description;
|
|
452
|
+
}
|
|
453
|
+
function renderPageDocument(page) {
|
|
454
|
+
const lines = [`# ${page.title}`, `URL: ${page.url}`];
|
|
455
|
+
if (page.description) lines.push(`Description: ${page.description}`);
|
|
456
|
+
lines.push("", page.rawContent ?? page.content);
|
|
457
|
+
return lines.join("\n");
|
|
458
|
+
}
|
|
459
|
+
function renderNavigationTree(tree) {
|
|
460
|
+
const lines = [`# ${tree.name}`, ""];
|
|
461
|
+
function visit(nodes, depth) {
|
|
462
|
+
const prefix = " ".repeat(depth);
|
|
463
|
+
for (const node of nodes) {
|
|
464
|
+
if (node.type === "page") {
|
|
465
|
+
lines.push(`${prefix}- ${node.name} (${node.url})`);
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
lines.push(`${prefix}- ${node.name}`);
|
|
469
|
+
if (node.index) lines.push(`${prefix} - Overview (${node.index.url})`);
|
|
470
|
+
visit(node.children, depth + 1);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
visit(tree.children, 0);
|
|
474
|
+
return lines.join("\n");
|
|
475
|
+
}
|
|
476
|
+
function slugToKey(slug) {
|
|
477
|
+
const normalized = normalizePathSegment(slug);
|
|
478
|
+
return normalized.length > 0 ? normalized.replace(/\//g, "-") : "index";
|
|
479
|
+
}
|
|
480
|
+
function toPageResourceUri(url) {
|
|
481
|
+
return `docs://${normalizePathSegment(url.replace(/^\//, "")) || "docs"}`;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
//#endregion
|
|
485
|
+
export { createDocsMcpHttpHandler, createDocsMcpServer, createFilesystemDocsMcpSource, normalizeDocsMcpRoute, resolveDocsMcpConfig, runDocsMcpStdio };
|
package/dist/server.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { o as DocsConfig } from "./types-
|
|
1
|
+
import { o as DocsConfig } from "./types-Bd3kyFF1.mjs";
|
|
2
|
+
import { DocsMcpHttpHandlers, DocsMcpNavigationNode, DocsMcpNavigationTree, DocsMcpPage, DocsMcpResolvedConfig, DocsMcpSource, createDocsMcpHttpHandler, createDocsMcpServer, createFilesystemDocsMcpSource, normalizeDocsMcpRoute, resolveDocsMcpConfig, runDocsMcpStdio } from "./mcp.mjs";
|
|
2
3
|
|
|
3
4
|
//#region src/api-reference.d.ts
|
|
4
5
|
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "HEAD";
|
|
@@ -35,4 +36,4 @@ declare function buildApiReferenceOpenApiDocumentAsync(config: DocsConfig, optio
|
|
|
35
36
|
declare function buildApiReferenceHtmlDocument(config: DocsConfig, options: BuildApiReferenceHtmlOptions): string;
|
|
36
37
|
declare function buildApiReferenceHtmlDocumentAsync(config: DocsConfig, options: BuildApiReferenceHtmlOptions): Promise<string>;
|
|
37
38
|
//#endregion
|
|
38
|
-
export { type ApiReferenceFramework, type ApiReferenceRoute, type ResolvedApiReferenceConfig, buildApiReferenceHtmlDocument, buildApiReferenceHtmlDocumentAsync, buildApiReferenceOpenApiDocument, buildApiReferenceOpenApiDocumentAsync, buildApiReferencePageTitle, buildApiReferenceScalarCss, resolveApiReferenceConfig };
|
|
39
|
+
export { type ApiReferenceFramework, type ApiReferenceRoute, type DocsMcpHttpHandlers, type DocsMcpNavigationNode, type DocsMcpNavigationTree, type DocsMcpPage, type DocsMcpResolvedConfig, type DocsMcpSource, type ResolvedApiReferenceConfig, buildApiReferenceHtmlDocument, buildApiReferenceHtmlDocumentAsync, buildApiReferenceOpenApiDocument, buildApiReferenceOpenApiDocumentAsync, buildApiReferencePageTitle, buildApiReferenceScalarCss, createDocsMcpHttpHandler, createDocsMcpServer, createFilesystemDocsMcpSource, normalizeDocsMcpRoute, resolveApiReferenceConfig, resolveDocsMcpConfig, runDocsMcpStdio };
|