Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 4 additions & 70 deletions docusaurus.config.ts
Original file line number Diff line number Diff line change
@@ -1,90 +1,24 @@
import path from 'path';
import { promises as fs } from 'fs';
import { pathToFileURL } from 'url';
import { themes as prismThemes } from 'prism-react-renderer';
import mcpServerPlugin from 'docusaurus-plugin-mcp-server';
import type { Config, Plugin, LoadContext } from '@docusaurus/types';
import type { Config } from '@docusaurus/types';
import type * as Preset from '@docusaurus/preset-classic';

// Absolute file:// URL works with the plugin's dynamic import() loader,
// which resolves relative paths against its own dist directory.
//
// Once https://github.com/scalvert/docusaurus-plugin-mcp-server/pull/78 lands,
// drop `mcp-providers/` and replace with `flexsearch: { tokenize: 'strict',
// context: false, ... }` directly in plugin options below.
const leanIndexerSpecifier = pathToFileURL(
path.join(__dirname, 'mcp-providers/lean-indexer.ts'),
).href;
import flexsearchConfig from './flexsearch.config';

// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)

// trailingSlash: false → Docusaurus emits pages as `path.html`, but
// docusaurus-plugin-mcp-server only scans `index.html`. Docusaurus runs all
// plugin postBuild hooks via Promise.all, so a separate shim plugin would
// race the MCP plugin. We wrap the MCP plugin instead and run the shim
// sequentially inside the same postBuild.
//
// Remove `shimHtmlForMcp` and `wrappedMcpPlugin` once this lands upstream:
// https://github.com/scalvert/docusaurus-plugin-mcp-server/pull/77
async function shimHtmlForMcp(outDir: string) {
const skip = new Set(['assets', 'img', 'static', 'mcp']);
async function walk(dir: string) {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
if (skip.has(entry.name)) continue;
await walk(path.join(dir, entry.name));
} else if (
entry.isFile() &&
entry.name.endsWith('.html') &&
entry.name !== 'index.html' &&
entry.name !== '404.html'
) {
const base = entry.name.slice(0, -'.html'.length);
const targetDir = path.join(dir, base);
const targetFile = path.join(targetDir, 'index.html');
try {
await fs.access(targetFile);
continue;
} catch {}
await fs.mkdir(targetDir, { recursive: true });
await fs.copyFile(path.join(dir, entry.name), targetFile);
}
}
}
await walk(outDir);
}

async function wrappedMcpPlugin(ctx: LoadContext, opts: unknown): Promise<Plugin> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const inner = await (mcpServerPlugin as any)(ctx, opts);
const originalPostBuild = inner.postBuild;
return {
...inner,
name: inner.name ?? 'docusaurus-plugin-mcp-server',
async postBuild(args: { outDir: string; [k: string]: unknown }) {
await shimHtmlForMcp(args.outDir);
if (originalPostBuild) {
await originalPostBuild.call(inner, args);
}
},
};
}

const config: Config = {
title: 'Справочный центр Хекслета',
tagline: 'Ответы на частые вопросы и инструкции для студентов',
favicon: 'img/favicon.ico',

plugins: [
[
wrappedMcpPlugin,
require.resolve('docusaurus-plugin-mcp-server'),
{
server: {
name: 'hexlet-help',
version: '1.0.0',
},
indexers: [leanIndexerSpecifier],
flexsearch: flexsearchConfig,
},
],
[
Expand Down
27 changes: 27 additions & 0 deletions flexsearch.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { FlexSearchConfig } from 'docusaurus-plugin-mcp-server';

// FlexSearch tuned for Russian content. The plugin's defaults are tuned for
// English (forward tokenize + bidirectional context + English stemmer) and
// produce an 80+ MB index on our docs.
//
// - tokenize: 'strict' indexes whole words only (no prefix explosion).
// - context: false drops the bidirectional context that bloats the index.
// - resolution: 3 is enough for relevance ranking on ~100 docs.
// - encode: lowercase split on Russian punctuation, no English stemmer.
//
// This config MUST be identical at build time (docusaurus.config.ts) and at
// runtime (worker/index.ts) — otherwise the runtime provider deserializes the
// index with the wrong shape and returns no results.
const flexsearchConfig: FlexSearchConfig = {
tokenize: 'strict',
resolution: 3,
context: false,
cache: 100,
encode: (str: string) =>
String(str)
.toLowerCase()
.split(/[\s\-_.,;:!?'"()[\]{}«»—–]+/)
.filter(Boolean),
};

export default flexsearchConfig;
161 changes: 0 additions & 161 deletions mcp-providers/lean-flexsearch.ts

This file was deleted.

39 changes: 0 additions & 39 deletions mcp-providers/lean-indexer.ts

This file was deleted.

60 changes: 0 additions & 60 deletions mcp-providers/lean-provider.ts

This file was deleted.

Loading