@farming-labs/docs 0.1.2 → 0.1.3

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.
@@ -4,6 +4,7 @@ import pc from "picocolors";
4
4
  //#region src/cli/index.ts
5
5
  const args = process.argv.slice(2);
6
6
  const command = args[0];
7
+ const subcommand = args[1];
7
8
  const UPGRADE_TAGS = ["latest", "beta"];
8
9
  /** Normalize command aliases like `upgrade@beta` into the base command + dist-tag. */
9
10
  function parseCommandAlias(rawCommand) {
@@ -18,7 +19,11 @@ function parseCommandAlias(rawCommand) {
18
19
  /** Parse flags like --template next, --name my-docs, --theme concrete, --entry docs, --framework astro (exported for tests). */
19
20
  function parseFlags(argv) {
20
21
  const flags = {};
21
- const booleanFlags = new Set(["api-reference"]);
22
+ const booleanFlags = new Set([
23
+ "api-reference",
24
+ "typesense",
25
+ "algolia"
26
+ ]);
22
27
  for (let i = 0; i < argv.length; i++) {
23
28
  const arg = argv[i];
24
29
  if (arg.startsWith("--") && arg.includes("=")) {
@@ -47,14 +52,38 @@ async function main() {
47
52
  apiRouteRoot: typeof flags["api-route-root"] === "string" ? flags["api-route-root"] : void 0
48
53
  };
49
54
  const mcpOptions = { configPath: typeof flags.config === "string" ? flags.config : void 0 };
55
+ const searchSyncOptions = {
56
+ configPath: typeof flags.config === "string" ? flags.config : void 0,
57
+ provider: typeof flags.provider === "string" ? flags.provider : void 0,
58
+ typesense: typeof flags.typesense === "boolean" ? flags.typesense : void 0,
59
+ algolia: typeof flags.algolia === "boolean" ? flags.algolia : void 0,
60
+ baseUrl: typeof flags["base-url"] === "string" ? flags["base-url"] : void 0,
61
+ collection: typeof flags.collection === "string" ? flags.collection : void 0,
62
+ apiKey: typeof flags["api-key"] === "string" ? flags["api-key"] : void 0,
63
+ adminApiKey: typeof flags["admin-api-key"] === "string" ? flags["admin-api-key"] : void 0,
64
+ mode: typeof flags.mode === "string" ? flags.mode : void 0,
65
+ ollamaModel: typeof flags["ollama-model"] === "string" ? flags["ollama-model"] : void 0,
66
+ ollamaBaseUrl: typeof flags["ollama-base-url"] === "string" ? flags["ollama-base-url"] : void 0,
67
+ appId: typeof flags["app-id"] === "string" ? flags["app-id"] : void 0,
68
+ indexName: typeof flags["index-name"] === "string" ? flags["index-name"] : void 0,
69
+ searchApiKey: typeof flags["search-api-key"] === "string" ? flags["search-api-key"] : void 0
70
+ };
50
71
  if (!parsedCommand.command || parsedCommand.command === "init") {
51
- const { init } = await import("../init-N0bZQFRd.mjs");
72
+ const { init } = await import("../init-C7kgy5hD.mjs");
52
73
  await init(initOptions);
53
74
  } else if (parsedCommand.command === "mcp") {
54
- const { runMcp } = await import("../mcp-8rCBy2-U.mjs");
75
+ const { runMcp } = await import("../mcp-aXyV1jPp.mjs");
55
76
  await runMcp(mcpOptions);
77
+ } else if (parsedCommand.command === "search" && subcommand === "sync") {
78
+ const { syncSearch } = await import("../search-ChhShKMO.mjs");
79
+ await syncSearch(searchSyncOptions);
80
+ } else if (parsedCommand.command === "search") {
81
+ console.error(pc.red(`Unknown search subcommand: ${subcommand ?? "(missing)"}`));
82
+ console.error();
83
+ printHelp();
84
+ process.exit(1);
56
85
  } else if (parsedCommand.command === "upgrade") {
57
- const { upgrade } = await import("../upgrade-BbEyR_JB.mjs");
86
+ const { upgrade } = await import("../upgrade-J_kkv-ti.mjs");
58
87
  await upgrade({
59
88
  framework: (typeof flags.framework === "string" ? flags.framework : void 0) ?? (args[1] && !args[1].startsWith("--") ? args[1] : void 0),
60
89
  tag: args.includes("--beta") ? "beta" : args.includes("--latest") ? "latest" : parsedCommand.tag ?? "latest"
@@ -78,6 +107,7 @@ ${pc.dim("Usage:")}
78
107
  ${pc.dim("Commands:")}
79
108
  ${pc.cyan("init")} Scaffold docs in your project (default)
80
109
  ${pc.cyan("mcp")} Run the built-in docs MCP server over stdio
110
+ ${pc.cyan("search")} Search utilities (${pc.dim("sync")} for external indexes)
81
111
  ${pc.cyan("upgrade")} Upgrade @farming-labs/* packages to latest (auto-detect or use --framework)
82
112
 
83
113
  ${pc.dim("Supported frameworks:")}
@@ -95,6 +125,24 @@ ${pc.dim("Options for init:")}
95
125
  ${pc.dim("Options for mcp:")}
96
126
  ${pc.cyan("--config <path>")} Use a custom docs config path instead of ${pc.dim("docs.config.ts[x]")}
97
127
 
128
+ ${pc.dim("Options for search sync:")}
129
+ ${pc.cyan("search sync --typesense")} Sync docs content to Typesense using env/flags
130
+ ${pc.cyan("search sync --algolia")} Sync docs content to Algolia using env/flags
131
+ ${pc.cyan("--config <path>")} Use a custom docs config path instead of ${pc.dim("docs.config.ts[x]")}
132
+ ${pc.cyan("--provider <name>")} Explicit provider (${pc.dim("typesense")}, ${pc.dim("algolia")})
133
+ ${pc.cyan("--typesense")} Shortcut for ${pc.cyan("--provider typesense")}
134
+ ${pc.cyan("--algolia")} Shortcut for ${pc.cyan("--provider algolia")}
135
+ ${pc.cyan("--base-url <url>")} Typesense base URL (or use ${pc.dim("TYPESENSE_URL")})
136
+ ${pc.cyan("--collection <name>")} Typesense collection name (default ${pc.dim("docs")})
137
+ ${pc.cyan("--api-key <key>")} Typesense search/api key (or use ${pc.dim("TYPESENSE_API_KEY")})
138
+ ${pc.cyan("--admin-api-key <key>")} Admin-capable sync key for Typesense/Algolia
139
+ ${pc.cyan("--mode <keyword|hybrid>")} Typesense mode (default ${pc.dim("keyword")})
140
+ ${pc.cyan("--ollama-model <name>")} Embeddings model for Typesense hybrid sync
141
+ ${pc.cyan("--ollama-base-url <url>")} Ollama base URL for hybrid embeddings
142
+ ${pc.cyan("--app-id <id>")} Algolia app id (or use ${pc.dim("ALGOLIA_APP_ID")})
143
+ ${pc.cyan("--index-name <name>")} Algolia index name (default ${pc.dim("docs")})
144
+ ${pc.cyan("--search-api-key <key>")} Algolia search key (or use ${pc.dim("ALGOLIA_SEARCH_API_KEY")})
145
+
98
146
  ${pc.dim("Options for upgrade:")}
99
147
  ${pc.cyan("--framework <name>")} Explicit framework (${pc.dim("next")}, ${pc.dim("tanstack-start")}, ${pc.dim("nuxt")}, ${pc.dim("sveltekit")}, ${pc.dim("astro")}); omit to auto-detect
100
148
  ${pc.cyan("--latest")} Install latest stable (default)
@@ -0,0 +1,95 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+
4
+ //#region src/cli/config.ts
5
+ const FILE_EXTS = [
6
+ "tsx",
7
+ "ts",
8
+ "jsx",
9
+ "js"
10
+ ];
11
+ function escapeRegExp(value) {
12
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13
+ }
14
+ function createPropertyPattern(key, valuePattern) {
15
+ return new RegExp(`\\b${escapeRegExp(key)}\\b\\s*:\\s*${valuePattern}`);
16
+ }
17
+ function resolveDocsConfigPath(rootDir, explicitPath) {
18
+ if (explicitPath) {
19
+ const resolvedPath = resolve(rootDir, explicitPath);
20
+ if (!existsSync(resolvedPath)) throw new Error(`Could not find docs config at ${explicitPath}.`);
21
+ return resolvedPath;
22
+ }
23
+ for (const ext of FILE_EXTS) {
24
+ const configPath = join(rootDir, `docs.config.${ext}`);
25
+ if (existsSync(configPath)) return configPath;
26
+ }
27
+ 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.");
28
+ }
29
+ function readStringProperty(content, key) {
30
+ return content.match(createPropertyPattern(key, `["']([^"']+)["']`))?.[1];
31
+ }
32
+ function readBooleanProperty(content, key) {
33
+ const match = content.match(createPropertyPattern(key, "(true|false)"));
34
+ return match ? match[1] === "true" : void 0;
35
+ }
36
+ function extractObjectLiteral(content, key) {
37
+ const keyIndex = content.search(new RegExp(`${key}\\s*:\\s*\\{`));
38
+ if (keyIndex === -1) return void 0;
39
+ const braceStart = content.indexOf("{", keyIndex);
40
+ if (braceStart === -1) return void 0;
41
+ let depth = 0;
42
+ for (let index = braceStart; index < content.length; index += 1) {
43
+ const char = content[index];
44
+ if (char === "{") {
45
+ depth += 1;
46
+ continue;
47
+ }
48
+ if (char !== "}") continue;
49
+ depth -= 1;
50
+ if (depth === 0) return content.slice(braceStart + 1, index);
51
+ }
52
+ }
53
+ function readNavTitle(content) {
54
+ const block = extractObjectLiteral(content, "nav");
55
+ if (!block) return void 0;
56
+ return readStringProperty(block, "title");
57
+ }
58
+ function resolveDocsContentDir(rootDir, content, entry) {
59
+ const configuredContentDir = readStringProperty(content, "contentDir");
60
+ if (configuredContentDir) return configuredContentDir;
61
+ const candidates = [
62
+ entry,
63
+ join("app", entry),
64
+ join("src", "app", entry)
65
+ ];
66
+ for (const candidate of candidates) if (existsSync(join(rootDir, candidate))) return candidate;
67
+ return entry;
68
+ }
69
+ function parseEnvValue(rawValue) {
70
+ const value = rawValue.trim();
71
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
72
+ return value;
73
+ }
74
+ function loadProjectEnv(rootDir) {
75
+ const env = {};
76
+ for (const filename of [".env", ".env.local"]) {
77
+ const fullPath = join(rootDir, filename);
78
+ if (!existsSync(fullPath)) continue;
79
+ const lines = readFileSync(fullPath, "utf-8").split(/\r?\n/);
80
+ for (const line of lines) {
81
+ const trimmed = line.trim();
82
+ if (!trimmed || trimmed.startsWith("#")) continue;
83
+ const equalsIndex = trimmed.indexOf("=");
84
+ if (equalsIndex === -1) continue;
85
+ const key = trimmed.slice(0, equalsIndex).trim();
86
+ const rawValue = trimmed.slice(equalsIndex + 1);
87
+ if (!key) continue;
88
+ env[key] = parseEnvValue(rawValue);
89
+ }
90
+ }
91
+ return env;
92
+ }
93
+
94
+ //#endregion
95
+ export { readStringProperty as a, readNavTitle as i, loadProjectEnv as n, resolveDocsConfigPath as o, readBooleanProperty as r, resolveDocsContentDir as s, extractObjectLiteral as t };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,5 @@
1
- import { A as SidebarConfig, C as OpenGraphImage, D as PageOpenGraph, E as PageFrontmatter, F as ThemeToggleConfig, I as TypographyConfig, L as UIConfig, M as SidebarNode, N as SidebarPageNode, O as PageTwitter, P as SidebarTree, S as OpenDocsProvider, T as PageActionsConfig, _ as GithubConfig, a as CopyMarkdownConfig, b as OGConfig, c as DocsFeedbackValue, d as DocsMcpToolsConfig, f as DocsMetadata, g as FontStyle, h as FeedbackConfig, i as CodeBlockCopyData, j as SidebarFolderNode, k as SidebarComponentProps, l as DocsI18nConfig, m as DocsTheme, n as ApiReferenceConfig, o as DocsConfig, p as DocsNav, r as BreadcrumbConfig, s as DocsFeedbackData, t as AIConfig, u as DocsMcpConfig, v as LastUpdatedConfig, w as OrderingItem, x as OpenDocsConfig, y as LlmsTxtConfig } from "./types-dqnMXLdw.mjs";
1
+ import { $ as UIConfig, A as GithubConfig, B as PageFrontmatter, C as DocsSearchQuery, D as DocsTheme, E as DocsSearchSourcePage, F as OpenDocsConfig, G as SidebarFolderNode, H as PageTwitter, I as OpenDocsProvider, J as SidebarTree, K as SidebarNode, L as OpenGraphImage, M as LlmsTxtConfig, N as McpDocsSearchConfig, O as FeedbackConfig, P as OGConfig, Q as TypographyConfig, R as OrderingItem, S as DocsSearchEmbeddingsConfig, T as DocsSearchResultType, U as SidebarComponentProps, V as PageOpenGraph, W as SidebarConfig, X as ThemeToggleConfig, Y as SimpleDocsSearchConfig, Z as TypesenseDocsSearchConfig, _ as DocsSearchAdapterContext, a as CodeBlockCopyData, b as DocsSearchConfig, c as DocsConfig, d as DocsI18nConfig, f as DocsMcpConfig, g as DocsSearchAdapter, h as DocsNav, i as BreadcrumbConfig, j as LastUpdatedConfig, k as FontStyle, l as DocsFeedbackData, m as DocsMetadata, n as AlgoliaDocsSearchConfig, o as CopyMarkdownConfig, p as DocsMcpToolsConfig, q as SidebarPageNode, r as ApiReferenceConfig, s as CustomDocsSearchConfig, t as AIConfig, u as DocsFeedbackValue, v as DocsSearchAdapterFactory, w as DocsSearchResult, x as DocsSearchDocument, y as DocsSearchChunkingConfig, z as PageActionsConfig } from "./types-BAulrjlV.mjs";
2
+ import { a as createSimpleSearchAdapter, c as resolveSearchRequestConfig, i as createMcpSearchAdapter, n as createAlgoliaSearchAdapter, o as createTypesenseSearchAdapter, r as createCustomSearchAdapter, s as performDocsSearch, t as buildDocsSearchDocuments } from "./search-KzREATdM.mjs";
2
3
 
3
4
  //#region src/define-docs.d.ts
4
5
  /**
@@ -97,4 +98,4 @@ declare function buildPageOpenGraph(page: Pick<PageFrontmatter, "title" | "descr
97
98
  */
98
99
  declare function buildPageTwitter(page: Pick<PageFrontmatter, "title" | "description" | "ogImage" | "openGraph" | "twitter">, ogConfig?: OGConfig, baseUrl?: string): PageTwitter | undefined;
99
100
  //#endregion
100
- export { type AIConfig, type ApiReferenceConfig, type BreadcrumbConfig, type CodeBlockCopyData, type CopyMarkdownConfig, type DocsConfig, type DocsFeedbackData, type DocsFeedbackValue, type DocsI18nConfig, type DocsMcpConfig, type DocsMcpToolsConfig, type DocsMetadata, type DocsNav, type DocsPathMatch, type DocsTheme, type FeedbackConfig, type FontStyle, type GithubConfig, type LastUpdatedConfig, type LlmsTxtConfig, type OGConfig, type OpenDocsConfig, type OpenDocsProvider, type OpenGraphImage, type OrderingItem, type PageActionsConfig, type PageFrontmatter, type PageOpenGraph, type PageTwitter, type ResolvedDocsI18n, type SidebarComponentProps, type SidebarConfig, type SidebarFolderNode, type SidebarNode, type SidebarPageNode, type SidebarTree, type ThemeToggleConfig, type TypographyConfig, type UIConfig, buildPageOpenGraph, buildPageTwitter, createTheme, deepMerge, defineDocs, extendTheme, resolveDocsI18n, resolveDocsLocale, resolveDocsPath, resolveOGImage, resolveTitle };
101
+ export { type AIConfig, type AlgoliaDocsSearchConfig, type ApiReferenceConfig, type BreadcrumbConfig, type CodeBlockCopyData, type CopyMarkdownConfig, type CustomDocsSearchConfig, type DocsConfig, type DocsFeedbackData, type DocsFeedbackValue, type DocsI18nConfig, type DocsMcpConfig, type DocsMcpToolsConfig, type DocsMetadata, type DocsNav, type DocsPathMatch, type DocsSearchAdapter, type DocsSearchAdapterContext, type DocsSearchAdapterFactory, type DocsSearchChunkingConfig, type DocsSearchConfig, type DocsSearchDocument, type DocsSearchEmbeddingsConfig, type DocsSearchQuery, type DocsSearchResult, type DocsSearchResultType, type DocsSearchSourcePage, type DocsTheme, type FeedbackConfig, type FontStyle, type GithubConfig, type LastUpdatedConfig, type LlmsTxtConfig, type McpDocsSearchConfig, type OGConfig, type OpenDocsConfig, type OpenDocsProvider, type OpenGraphImage, type OrderingItem, type PageActionsConfig, type PageFrontmatter, type PageOpenGraph, type PageTwitter, type ResolvedDocsI18n, type SidebarComponentProps, type SidebarConfig, type SidebarFolderNode, type SidebarNode, type SidebarPageNode, type SidebarTree, type SimpleDocsSearchConfig, type ThemeToggleConfig, type TypesenseDocsSearchConfig, type TypographyConfig, type UIConfig, buildDocsSearchDocuments, buildPageOpenGraph, buildPageTwitter, createAlgoliaSearchAdapter, createCustomSearchAdapter, createMcpSearchAdapter, createSimpleSearchAdapter, createTheme, createTypesenseSearchAdapter, deepMerge, defineDocs, extendTheme, performDocsSearch, resolveDocsI18n, resolveDocsLocale, resolveDocsPath, resolveOGImage, resolveSearchRequestConfig, resolveTitle };
package/dist/index.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ import { a as createSimpleSearchAdapter, c as resolveSearchRequestConfig, i as createMcpSearchAdapter, n as createAlgoliaSearchAdapter, o as createTypesenseSearchAdapter, r as createCustomSearchAdapter, s as performDocsSearch, t as buildDocsSearchDocuments } from "./search-BS6C5N1i.mjs";
2
+
1
3
  //#region src/define-docs.ts
2
4
  /**
3
5
  * Define docs configuration. Validates and returns the config.
@@ -16,6 +18,7 @@ function defineDocs(config) {
16
18
  components: config.components,
17
19
  onCopyClick: config.onCopyClick,
18
20
  feedback: config.feedback,
21
+ search: config.search,
19
22
  mcp: config.mcp,
20
23
  icons: config.icons,
21
24
  pageActions: config.pageActions,
@@ -224,4 +227,4 @@ function buildPageTwitter(page, ogConfig, baseUrl) {
224
227
  }
225
228
 
226
229
  //#endregion
227
- export { buildPageOpenGraph, buildPageTwitter, createTheme, deepMerge, defineDocs, extendTheme, resolveDocsI18n, resolveDocsLocale, resolveDocsPath, resolveOGImage, resolveTitle };
230
+ export { buildDocsSearchDocuments, buildPageOpenGraph, buildPageTwitter, createAlgoliaSearchAdapter, createCustomSearchAdapter, createMcpSearchAdapter, createSimpleSearchAdapter, createTheme, createTypesenseSearchAdapter, deepMerge, defineDocs, extendTheme, performDocsSearch, resolveDocsI18n, resolveDocsLocale, resolveDocsPath, resolveOGImage, resolveSearchRequestConfig, resolveTitle };
@@ -1,4 +1,4 @@
1
- import { a as devInstallCommand, c as installCommand, d as writeFileSafe, i as detectPackageManagerFromLockfile, l as readFileSafe, n as detectGlobalCssFiles, o as exec, r as detectNextAppDir, s as fileExists, t as detectFramework, u as spawnAndWaitFor } from "./utils-D5Wn7Q5E.mjs";
1
+ import { a as devInstallCommand, c as installCommand, d as writeFileSafe, i as detectPackageManagerFromLockfile, l as readFileSafe, n as detectGlobalCssFiles, o as exec, r as detectNextAppDir, s as fileExists, t as detectFramework, u as spawnAndWaitFor } from "./utils-CRhME2g-.mjs";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import pc from "picocolors";
@@ -0,0 +1,46 @@
1
+ import "./api-reference-DlfH-Y9c.mjs";
2
+ import { createFilesystemDocsMcpSource, resolveDocsMcpConfig, runDocsMcpStdio } from "./mcp.mjs";
3
+ import "./server.mjs";
4
+ import { a as readStringProperty, i as readNavTitle, o as resolveDocsConfigPath, r as readBooleanProperty, s as resolveDocsContentDir, t as extractObjectLiteral } from "./config-CSywk3ou.mjs";
5
+ import { readFileSync } from "node:fs";
6
+
7
+ //#region src/cli/mcp.ts
8
+ async function runMcp(options = {}) {
9
+ const rootDir = process.cwd();
10
+ const content = readFileSync(resolveDocsConfigPath(rootDir, options.configPath), "utf-8");
11
+ const entry = readStringProperty(content, "entry") ?? "docs";
12
+ const contentDir = resolveDocsContentDir(rootDir, content, entry);
13
+ const navTitle = readNavTitle(content);
14
+ const mcp = readMcpConfig(content);
15
+ await runDocsMcpStdio({
16
+ source: createFilesystemDocsMcpSource({
17
+ rootDir,
18
+ entry,
19
+ contentDir,
20
+ siteTitle: navTitle ?? "Documentation"
21
+ }),
22
+ mcp: resolveDocsMcpConfig(mcp ?? true, { defaultName: navTitle ?? "@farming-labs/docs" }),
23
+ defaultName: navTitle ?? "@farming-labs/docs"
24
+ });
25
+ }
26
+ function readMcpConfig(content) {
27
+ if (content.match(/mcp\s*:\s*false/)) return false;
28
+ if (content.match(/mcp\s*:\s*true/)) return true;
29
+ const block = extractObjectLiteral(content, "mcp");
30
+ if (!block) return void 0;
31
+ return {
32
+ enabled: readBooleanProperty(block, "enabled"),
33
+ route: readStringProperty(block, "route"),
34
+ name: readStringProperty(block, "name"),
35
+ version: readStringProperty(block, "version"),
36
+ tools: {
37
+ listPages: readBooleanProperty(block, "listPages"),
38
+ readPage: readBooleanProperty(block, "readPage"),
39
+ searchDocs: readBooleanProperty(block, "searchDocs"),
40
+ getNavigation: readBooleanProperty(block, "getNavigation")
41
+ }
42
+ };
43
+ }
44
+
45
+ //#endregion
46
+ export { runMcp };
package/dist/mcp.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { u as DocsMcpConfig, w as OrderingItem } from "./types-dqnMXLdw.mjs";
1
+ import { R as OrderingItem, b as DocsSearchConfig, f as DocsMcpConfig } from "./types-BAulrjlV.mjs";
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
 
4
4
  //#region src/mcp.d.ts
@@ -62,6 +62,7 @@ interface DocsMcpHttpHandlers {
62
62
  interface CreateDocsMcpServerOptions {
63
63
  source: DocsMcpSource;
64
64
  mcp?: boolean | DocsMcpConfig;
65
+ search?: boolean | DocsSearchConfig;
65
66
  defaultName?: string;
66
67
  defaultVersion?: string;
67
68
  }
package/dist/mcp.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { s as performDocsSearch } from "./search-BS6C5N1i.mjs";
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
3
4
  import { randomUUID } from "node:crypto";
@@ -88,6 +89,7 @@ async function createDocsMcpServer(options) {
88
89
  defaultName: options.defaultName ?? options.source.siteTitle ?? DEFAULT_MCP_NAME,
89
90
  defaultVersion: options.defaultVersion
90
91
  });
92
+ const toolSearchConfig = resolveMcpToolSearchConfig(options.search, resolved.route);
91
93
  const server = new McpServer({
92
94
  name: resolved.name,
93
95
  version: resolved.version
@@ -144,7 +146,14 @@ async function createDocsMcpServer(options) {
144
146
  inputSchema: searchDocsInputSchema,
145
147
  annotations: { readOnlyHint: true }
146
148
  }, async ({ query, limit, locale }) => {
147
- const results = searchDocsPages(dedupePages(await options.source.getPages(locale)), query, limit ?? 10);
149
+ const results = await performDocsSearch({
150
+ pages: toSearchSourcePages(dedupePages(await options.source.getPages(locale))),
151
+ query,
152
+ search: toolSearchConfig ?? true,
153
+ locale,
154
+ siteTitle: options.source.siteTitle,
155
+ limit: limit ?? 10
156
+ });
148
157
  return { content: [{
149
158
  type: "text",
150
159
  text: JSON.stringify({ results }, null, 2)
@@ -382,6 +391,31 @@ function dedupePages(pages) {
382
391
  for (const page of pages) seen.set(page.url, page);
383
392
  return [...seen.values()];
384
393
  }
394
+ function toSearchSourcePages(pages) {
395
+ return pages.map((page) => ({
396
+ title: page.title,
397
+ url: page.url,
398
+ content: page.content,
399
+ rawContent: page.rawContent,
400
+ description: page.description
401
+ }));
402
+ }
403
+ function isSelfMcpSearchEndpoint(search, route) {
404
+ if (!search || search === true || typeof search !== "object" || search.provider !== "mcp") return false;
405
+ const endpoint = search.endpoint.trim();
406
+ if (!endpoint.startsWith("/")) return false;
407
+ return normalizeDocsMcpRoute(endpoint) === normalizeDocsMcpRoute(route);
408
+ }
409
+ function resolveMcpToolSearchConfig(search, route) {
410
+ if (!isSelfMcpSearchEndpoint(search, route)) return search;
411
+ const config = search;
412
+ return {
413
+ provider: "simple",
414
+ enabled: config.enabled,
415
+ maxResults: config.maxResults,
416
+ chunking: config.chunking
417
+ };
418
+ }
385
419
  function toPageSummaries(pages) {
386
420
  return pages.map((page) => ({
387
421
  slug: page.slug,
@@ -418,38 +452,6 @@ function normalizeUrlPath(value) {
418
452
  if (normalized === "/") return normalized;
419
453
  return normalized.replace(/\/+$/, "");
420
454
  }
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
455
  function renderPageDocument(page) {
454
456
  const lines = [`# ${page.title}`, `URL: ${page.url}`];
455
457
  if (page.description) lines.push(`Description: ${page.description}`);