@farming-labs/docs 0.1.1-beta.4 → 0.1.2
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 +12 -0
- package/dist/cli/index.mjs +119 -0
- package/dist/index.d.mts +100 -0
- package/dist/index.mjs +227 -0
- package/dist/init-N0bZQFRd.mjs +3277 -0
- package/dist/mcp-8rCBy2-U.mjs +93 -0
- package/dist/mcp.d.mts +86 -0
- package/dist/mcp.mjs +485 -0
- package/dist/server.d.mts +39 -0
- package/dist/server.mjs +4 -0
- package/dist/types-dqnMXLdw.d.mts +1484 -0
- package/dist/upgrade-BbEyR_JB.mjs +138 -0
- package/dist/utils-D5Wn7Q5E.mjs +145 -0
- package/package.json +10 -3
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import "./api-reference-wh4_pwG8.mjs";
|
|
2
|
+
import { createFilesystemDocsMcpSource, resolveDocsMcpConfig, runDocsMcpStdio } from "./mcp.mjs";
|
|
3
|
+
import "./server.mjs";
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
|
|
7
|
+
//#region src/cli/mcp.ts
|
|
8
|
+
const FILE_EXTS = [
|
|
9
|
+
"tsx",
|
|
10
|
+
"ts",
|
|
11
|
+
"jsx",
|
|
12
|
+
"js"
|
|
13
|
+
];
|
|
14
|
+
async function runMcp(options = {}) {
|
|
15
|
+
const rootDir = process.cwd();
|
|
16
|
+
const content = readFileSync(resolveDocsConfigPath(rootDir, options.configPath), "utf-8");
|
|
17
|
+
const entry = readStringProperty(content, "entry") ?? "docs";
|
|
18
|
+
const contentDir = readStringProperty(content, "contentDir") ?? entry;
|
|
19
|
+
const navTitle = readNavTitle(content);
|
|
20
|
+
const mcp = readMcpConfig(content);
|
|
21
|
+
await runDocsMcpStdio({
|
|
22
|
+
source: createFilesystemDocsMcpSource({
|
|
23
|
+
rootDir,
|
|
24
|
+
entry,
|
|
25
|
+
contentDir,
|
|
26
|
+
siteTitle: navTitle ?? "Documentation"
|
|
27
|
+
}),
|
|
28
|
+
mcp: resolveDocsMcpConfig(mcp ?? true, { defaultName: navTitle ?? "@farming-labs/docs" }),
|
|
29
|
+
defaultName: navTitle ?? "@farming-labs/docs"
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function resolveDocsConfigPath(rootDir, explicitPath) {
|
|
33
|
+
if (explicitPath) {
|
|
34
|
+
const resolvedPath = resolve(rootDir, explicitPath);
|
|
35
|
+
if (!existsSync(resolvedPath)) throw new Error(`Could not find docs config at ${explicitPath}.`);
|
|
36
|
+
return resolvedPath;
|
|
37
|
+
}
|
|
38
|
+
for (const ext of FILE_EXTS) {
|
|
39
|
+
const configPath = join(rootDir, `docs.config.${ext}`);
|
|
40
|
+
if (existsSync(configPath)) return configPath;
|
|
41
|
+
}
|
|
42
|
+
throw new Error("Could not find docs.config.ts or docs.config.tsx in the current project. Use --config to point at a custom path.");
|
|
43
|
+
}
|
|
44
|
+
function readStringProperty(content, key) {
|
|
45
|
+
return content.match(new RegExp(`${key}\\s*:\\s*["']([^"']+)["']`))?.[1];
|
|
46
|
+
}
|
|
47
|
+
function readNavTitle(content) {
|
|
48
|
+
const block = extractObjectLiteral(content, "nav");
|
|
49
|
+
if (!block) return void 0;
|
|
50
|
+
return readStringProperty(block, "title");
|
|
51
|
+
}
|
|
52
|
+
function readMcpConfig(content) {
|
|
53
|
+
if (content.match(/mcp\s*:\s*false/)) return false;
|
|
54
|
+
if (content.match(/mcp\s*:\s*true/)) return true;
|
|
55
|
+
const block = extractObjectLiteral(content, "mcp");
|
|
56
|
+
if (!block) return void 0;
|
|
57
|
+
return {
|
|
58
|
+
enabled: readBooleanProperty(block, "enabled"),
|
|
59
|
+
route: readStringProperty(block, "route"),
|
|
60
|
+
name: readStringProperty(block, "name"),
|
|
61
|
+
version: readStringProperty(block, "version"),
|
|
62
|
+
tools: {
|
|
63
|
+
listPages: readBooleanProperty(block, "listPages"),
|
|
64
|
+
readPage: readBooleanProperty(block, "readPage"),
|
|
65
|
+
searchDocs: readBooleanProperty(block, "searchDocs"),
|
|
66
|
+
getNavigation: readBooleanProperty(block, "getNavigation")
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function readBooleanProperty(content, key) {
|
|
71
|
+
const match = content.match(new RegExp(`${key}\\s*:\\s*(true|false)`));
|
|
72
|
+
return match ? match[1] === "true" : void 0;
|
|
73
|
+
}
|
|
74
|
+
function extractObjectLiteral(content, key) {
|
|
75
|
+
const keyIndex = content.search(new RegExp(`${key}\\s*:\\s*\\{`));
|
|
76
|
+
if (keyIndex === -1) return void 0;
|
|
77
|
+
const braceStart = content.indexOf("{", keyIndex);
|
|
78
|
+
if (braceStart === -1) return void 0;
|
|
79
|
+
let depth = 0;
|
|
80
|
+
for (let index = braceStart; index < content.length; index += 1) {
|
|
81
|
+
const char = content[index];
|
|
82
|
+
if (char === "{") {
|
|
83
|
+
depth += 1;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (char !== "}") continue;
|
|
87
|
+
depth -= 1;
|
|
88
|
+
if (depth === 0) return content.slice(braceStart + 1, index);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
//#endregion
|
|
93
|
+
export { runMcp };
|
package/dist/mcp.d.mts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { u as DocsMcpConfig, w as OrderingItem } from "./types-dqnMXLdw.mjs";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
|
|
4
|
+
//#region src/mcp.d.ts
|
|
5
|
+
interface DocsMcpPage {
|
|
6
|
+
slug: string;
|
|
7
|
+
url: string;
|
|
8
|
+
title: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
icon?: string;
|
|
11
|
+
content: string;
|
|
12
|
+
rawContent?: string;
|
|
13
|
+
}
|
|
14
|
+
interface DocsMcpPageNode {
|
|
15
|
+
type: "page";
|
|
16
|
+
name: string;
|
|
17
|
+
url: string;
|
|
18
|
+
icon?: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
}
|
|
21
|
+
interface DocsMcpFolderNode {
|
|
22
|
+
type: "folder";
|
|
23
|
+
name: string;
|
|
24
|
+
icon?: string;
|
|
25
|
+
index?: DocsMcpPageNode;
|
|
26
|
+
children: DocsMcpNavigationNode[];
|
|
27
|
+
}
|
|
28
|
+
type DocsMcpNavigationNode = DocsMcpPageNode | DocsMcpFolderNode;
|
|
29
|
+
interface DocsMcpNavigationTree {
|
|
30
|
+
name: string;
|
|
31
|
+
children: DocsMcpNavigationNode[];
|
|
32
|
+
}
|
|
33
|
+
interface DocsMcpSource {
|
|
34
|
+
entry?: string;
|
|
35
|
+
siteTitle?: string;
|
|
36
|
+
getPages(locale?: string): DocsMcpPage[] | Promise<DocsMcpPage[]>;
|
|
37
|
+
getNavigation(locale?: string): DocsMcpNavigationTree | Promise<DocsMcpNavigationTree>;
|
|
38
|
+
}
|
|
39
|
+
interface DocsMcpResolvedConfig {
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
route: string;
|
|
42
|
+
name: string;
|
|
43
|
+
version: string;
|
|
44
|
+
tools: {
|
|
45
|
+
listPages: boolean;
|
|
46
|
+
readPage: boolean;
|
|
47
|
+
searchDocs: boolean;
|
|
48
|
+
getNavigation: boolean;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
interface DocsMcpHttpHandlers {
|
|
52
|
+
GET: (context: {
|
|
53
|
+
request: Request;
|
|
54
|
+
}) => Promise<Response>;
|
|
55
|
+
POST: (context: {
|
|
56
|
+
request: Request;
|
|
57
|
+
}) => Promise<Response>;
|
|
58
|
+
DELETE: (context: {
|
|
59
|
+
request: Request;
|
|
60
|
+
}) => Promise<Response>;
|
|
61
|
+
}
|
|
62
|
+
interface CreateDocsMcpServerOptions {
|
|
63
|
+
source: DocsMcpSource;
|
|
64
|
+
mcp?: boolean | DocsMcpConfig;
|
|
65
|
+
defaultName?: string;
|
|
66
|
+
defaultVersion?: string;
|
|
67
|
+
}
|
|
68
|
+
interface CreateFilesystemDocsMcpSourceOptions {
|
|
69
|
+
rootDir?: string;
|
|
70
|
+
entry?: string;
|
|
71
|
+
contentDir?: string;
|
|
72
|
+
siteTitle?: string;
|
|
73
|
+
ordering?: "alphabetical" | "numeric" | OrderingItem[];
|
|
74
|
+
}
|
|
75
|
+
declare function normalizeDocsMcpRoute(route?: string): string;
|
|
76
|
+
declare function resolveDocsMcpConfig(mcp?: boolean | DocsMcpConfig, defaults?: {
|
|
77
|
+
defaultName?: string;
|
|
78
|
+
defaultVersion?: string;
|
|
79
|
+
defaultRoute?: string;
|
|
80
|
+
}): DocsMcpResolvedConfig;
|
|
81
|
+
declare function createFilesystemDocsMcpSource(options?: CreateFilesystemDocsMcpSourceOptions): DocsMcpSource;
|
|
82
|
+
declare function createDocsMcpServer(options: CreateDocsMcpServerOptions): Promise<McpServer>;
|
|
83
|
+
declare function createDocsMcpHttpHandler(options: CreateDocsMcpServerOptions): DocsMcpHttpHandlers;
|
|
84
|
+
declare function runDocsMcpStdio(options: CreateDocsMcpServerOptions): Promise<void>;
|
|
85
|
+
//#endregion
|
|
86
|
+
export { DocsMcpFolderNode, DocsMcpHttpHandlers, DocsMcpNavigationNode, DocsMcpNavigationTree, DocsMcpPage, DocsMcpPageNode, DocsMcpResolvedConfig, DocsMcpSource, createDocsMcpHttpHandler, createDocsMcpServer, createFilesystemDocsMcpSource, normalizeDocsMcpRoute, resolveDocsMcpConfig, runDocsMcpStdio };
|
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.js";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
8
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
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 };
|