@lichtblick/docs-mcp 0.0.0 → 0.1.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/CHANGELOG.md +29 -0
- package/LICENSE +373 -0
- package/README.md +153 -0
- package/dist/api/extension-api.js +137 -0
- package/dist/check.js +26 -0
- package/dist/config.js +13 -0
- package/dist/docs/llms-txt.js +64 -0
- package/dist/docs/markdown.js +42 -0
- package/dist/docs/source.js +44 -0
- package/dist/docs/store.js +169 -0
- package/dist/index.js +38 -0
- package/dist/logging.js +13 -0
- package/dist/server.js +174 -0
- package/npm-shrinkwrap.json +6756 -0
- package/package.json +71 -3
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (C) 2023-2026 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)<lichtblick@bmwgroup.com>
|
|
2
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
6
|
+
import ts from "typescript";
|
|
7
|
+
const KINDS = new Map([
|
|
8
|
+
[ts.SyntaxKind.InterfaceDeclaration, "interface"],
|
|
9
|
+
[ts.SyntaxKind.TypeAliasDeclaration, "type"],
|
|
10
|
+
[ts.SyntaxKind.FunctionDeclaration, "function"],
|
|
11
|
+
[ts.SyntaxKind.ClassDeclaration, "class"],
|
|
12
|
+
[ts.SyntaxKind.EnumDeclaration, "enum"],
|
|
13
|
+
[ts.SyntaxKind.VariableStatement, "const"],
|
|
14
|
+
]);
|
|
15
|
+
/** Reads the public API of `@lichtblick/suite` from the TypeScript sources it ships. */
|
|
16
|
+
export class ExtensionApi {
|
|
17
|
+
#packageDir;
|
|
18
|
+
#loaded;
|
|
19
|
+
constructor(packageDir = resolveSuitePackageDir()) {
|
|
20
|
+
this.#packageDir = packageDir;
|
|
21
|
+
}
|
|
22
|
+
async load() {
|
|
23
|
+
if (!this.#loaded) {
|
|
24
|
+
this.#loaded = loadSuiteApi(this.#packageDir);
|
|
25
|
+
this.#loaded.catch(() => (this.#loaded = undefined));
|
|
26
|
+
}
|
|
27
|
+
return await this.#loaded;
|
|
28
|
+
}
|
|
29
|
+
async get(name) {
|
|
30
|
+
const { symbols } = await this.load();
|
|
31
|
+
const lower = name.toLowerCase();
|
|
32
|
+
return (symbols.get(name) ??
|
|
33
|
+
[...symbols.values()].find((symbol) => symbol.name.toLowerCase() === lower));
|
|
34
|
+
}
|
|
35
|
+
async suggest(query, max = 15) {
|
|
36
|
+
const { symbols } = await this.load();
|
|
37
|
+
const lower = query.toLowerCase();
|
|
38
|
+
return [...symbols.keys()]
|
|
39
|
+
.filter((name) => name.toLowerCase().includes(lower))
|
|
40
|
+
.slice(0, max);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function resolveSuitePackageDir() {
|
|
44
|
+
return dirname(createRequire(import.meta.url).resolve("@lichtblick/suite/package.json"));
|
|
45
|
+
}
|
|
46
|
+
export async function loadSuiteApi(packageDir) {
|
|
47
|
+
const { version } = JSON.parse(await readFile(join(packageDir, "package.json"), "utf8"));
|
|
48
|
+
const srcDir = join(packageDir, "src");
|
|
49
|
+
const modules = new Map();
|
|
50
|
+
const parse = async (file) => {
|
|
51
|
+
let parsed = modules.get(file);
|
|
52
|
+
if (!parsed) {
|
|
53
|
+
parsed = readFile(file, "utf8").then((text) => parseModule(file, text));
|
|
54
|
+
modules.set(file, parsed);
|
|
55
|
+
}
|
|
56
|
+
return await parsed;
|
|
57
|
+
};
|
|
58
|
+
const entry = await parse(join(srcDir, "index.ts"));
|
|
59
|
+
const symbols = new Map(entry.declarations);
|
|
60
|
+
const reExported = await Promise.all(entry.reExports.map(async ({ name, exportedAs, from }) => {
|
|
61
|
+
const symbol = (await parse(resolve(srcDir, `${from}.ts`))).declarations.get(name);
|
|
62
|
+
return symbol && { ...symbol, name: exportedAs };
|
|
63
|
+
}));
|
|
64
|
+
for (const symbol of reExported) {
|
|
65
|
+
if (symbol) {
|
|
66
|
+
symbols.set(symbol.name, symbol);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { version, symbols };
|
|
70
|
+
}
|
|
71
|
+
export function parseModule(fileName, text) {
|
|
72
|
+
const sourceFile = ts.createSourceFile(fileName, text, ts.ScriptTarget.Latest, true);
|
|
73
|
+
const declarations = new Map();
|
|
74
|
+
const reExports = [];
|
|
75
|
+
for (const statement of sourceFile.statements) {
|
|
76
|
+
if (ts.isExportDeclaration(statement)) {
|
|
77
|
+
const { exportClause, moduleSpecifier } = statement;
|
|
78
|
+
if (exportClause &&
|
|
79
|
+
ts.isNamedExports(exportClause) &&
|
|
80
|
+
moduleSpecifier &&
|
|
81
|
+
ts.isStringLiteral(moduleSpecifier)) {
|
|
82
|
+
for (const element of exportClause.elements) {
|
|
83
|
+
reExports.push({
|
|
84
|
+
name: (element.propertyName ?? element.name).text,
|
|
85
|
+
exportedAs: element.name.text,
|
|
86
|
+
from: moduleSpecifier.text,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const kind = KINDS.get(statement.kind);
|
|
93
|
+
if (!kind || !isExported(statement)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const source = [
|
|
97
|
+
attachedComments(sourceFile, statement),
|
|
98
|
+
statement.getText(sourceFile),
|
|
99
|
+
]
|
|
100
|
+
.filter(Boolean)
|
|
101
|
+
.join("\n");
|
|
102
|
+
for (const name of declaredNames(statement)) {
|
|
103
|
+
declarations.set(name, { name, kind, file: basename(fileName), source });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { declarations, reExports };
|
|
107
|
+
}
|
|
108
|
+
function isExported(statement) {
|
|
109
|
+
return (ts.canHaveModifiers(statement) &&
|
|
110
|
+
(ts
|
|
111
|
+
.getModifiers(statement)
|
|
112
|
+
?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ??
|
|
113
|
+
false));
|
|
114
|
+
}
|
|
115
|
+
function declaredNames(statement) {
|
|
116
|
+
if (ts.isVariableStatement(statement)) {
|
|
117
|
+
return statement.declarationList.declarations.flatMap((declaration) => ts.isIdentifier(declaration.name) ? [declaration.name.text] : []);
|
|
118
|
+
}
|
|
119
|
+
const { name } = statement;
|
|
120
|
+
return name && ts.isIdentifier(name) ? [name.text] : [];
|
|
121
|
+
}
|
|
122
|
+
// Leading comments separated from the declaration by a blank line (e.g. license headers) are not attached.
|
|
123
|
+
function attachedComments(sourceFile, node) {
|
|
124
|
+
const text = sourceFile.text;
|
|
125
|
+
const ranges = ts.getLeadingCommentRanges(text, node.getFullStart()) ?? [];
|
|
126
|
+
const attached = [];
|
|
127
|
+
let next = node.getStart(sourceFile);
|
|
128
|
+
for (const range of ranges.toReversed()) {
|
|
129
|
+
if (text.slice(range.end, next).split("\n").length > 2) {
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
attached.unshift(text.slice(range.pos, range.end));
|
|
133
|
+
next = range.pos;
|
|
134
|
+
}
|
|
135
|
+
return attached.join("\n");
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=extension-api.js.map
|
package/dist/check.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (C) 2023-2026 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)<lichtblick@bmwgroup.com>
|
|
2
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
import { createDocSource } from "./docs/source.js";
|
|
4
|
+
import { DocsStore } from "./docs/store.js";
|
|
5
|
+
import { errorMessage } from "./logging.js";
|
|
6
|
+
/** Verifies that every page listed in llms.txt resolves to its .md page; returns the process exit code. */
|
|
7
|
+
export async function runCheck(config, print = (line) => process.stderr.write(`${line}\n`)) {
|
|
8
|
+
const source = config.docsSource;
|
|
9
|
+
try {
|
|
10
|
+
const { pages, failures } = await new DocsStore(createDocSource(source), config.siteUrl).verify();
|
|
11
|
+
if (pages === 0) {
|
|
12
|
+
print(`FAIL: llms.txt from ${source} lists no pages`);
|
|
13
|
+
return 1;
|
|
14
|
+
}
|
|
15
|
+
for (const { path, reason } of failures) {
|
|
16
|
+
print(`FAIL ${path}: ${reason}`);
|
|
17
|
+
}
|
|
18
|
+
print(`${pages - failures.length}/${pages} pages resolved (source: ${source})`);
|
|
19
|
+
return failures.length === 0 ? 0 : 1;
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
print(`FAIL: could not read llms.txt from ${source}: ${errorMessage(error)}`);
|
|
23
|
+
return 1;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=check.js.map
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (C) 2023-2026 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)<lichtblick@bmwgroup.com>
|
|
2
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
import { withTrailingSlash } from "./docs/source.js";
|
|
4
|
+
const DEFAULT_SITE_URL = "https://lichtblick-suite.github.io/docs/";
|
|
5
|
+
export function loadConfig(env = process.env) {
|
|
6
|
+
const siteUrl = new URL(withTrailingSlash(env.LICHTBLICK_DOCS_SITE_URL ?? DEFAULT_SITE_URL));
|
|
7
|
+
return {
|
|
8
|
+
siteUrl,
|
|
9
|
+
docsSource: env.LICHTBLICK_DOCS_SOURCE ?? siteUrl.href,
|
|
10
|
+
log: env.LICHTBLICK_DOCS_LOG?.trim().toLowerCase() !== "off",
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (C) 2023-2026 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)<lichtblick@bmwgroup.com>
|
|
2
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
const LINK_LINE = /^- \[(?<title>[^\]]+)\]\((?<url>[^)\s]+)\)(?<rest>.*)$/;
|
|
4
|
+
export function parseLlmsTxt(text, siteUrl) {
|
|
5
|
+
const entries = [];
|
|
6
|
+
let section = "";
|
|
7
|
+
for (const rawLine of text.split("\n")) {
|
|
8
|
+
const line = rawLine.trim();
|
|
9
|
+
if (line.startsWith("## ")) {
|
|
10
|
+
section = line.slice(3).trim();
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
const groups = LINK_LINE.exec(line)?.groups;
|
|
14
|
+
if (!groups?.title || !groups.url) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const path = toPagePath(groups.url, siteUrl);
|
|
18
|
+
if (!path) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
entries.push({
|
|
22
|
+
path,
|
|
23
|
+
title: groups.title,
|
|
24
|
+
url: new URL(path, siteUrl).href,
|
|
25
|
+
description: groups.rest?.startsWith(":") === true
|
|
26
|
+
? cleanDescription(groups.rest.slice(1))
|
|
27
|
+
: "",
|
|
28
|
+
section,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return entries;
|
|
32
|
+
}
|
|
33
|
+
/** Returns the page path for URLs under `siteUrl`, or undefined for external links. */
|
|
34
|
+
export function toPagePath(url, siteUrl) {
|
|
35
|
+
let parsed;
|
|
36
|
+
try {
|
|
37
|
+
parsed = new URL(url, siteUrl);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
if (parsed.origin !== siteUrl.origin ||
|
|
43
|
+
!parsed.pathname.startsWith(siteUrl.pathname)) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
return (trimSlashes(parsed.pathname.slice(siteUrl.pathname.length)) || undefined);
|
|
47
|
+
}
|
|
48
|
+
export function trimSlashes(value) {
|
|
49
|
+
let start = 0;
|
|
50
|
+
let end = value.length;
|
|
51
|
+
while (start < end && value[start] === "/") {
|
|
52
|
+
start++;
|
|
53
|
+
}
|
|
54
|
+
while (end > start && value[end - 1] === "/") {
|
|
55
|
+
end--;
|
|
56
|
+
}
|
|
57
|
+
return value.slice(start, end);
|
|
58
|
+
}
|
|
59
|
+
// docusaurus-plugin-llms falls back to the first body line, which is sometimes an admonition or HTML tag.
|
|
60
|
+
function cleanDescription(description) {
|
|
61
|
+
const trimmed = description.trim();
|
|
62
|
+
return trimmed.startsWith("<") || trimmed.startsWith(":::") ? "" : trimmed;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=llms-txt.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (C) 2023-2026 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)<lichtblick@bmwgroup.com>
|
|
2
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
const FENCE = /^\s*(```|~~~)/;
|
|
4
|
+
const HEADING = /^#{2,3} (.*)$/;
|
|
5
|
+
/** Splits a page into its intro plus one section per `##`/`###` heading. */
|
|
6
|
+
export function splitSections(markdown) {
|
|
7
|
+
const sections = [];
|
|
8
|
+
let heading = "";
|
|
9
|
+
let lines = [];
|
|
10
|
+
let inFence = false;
|
|
11
|
+
const flush = () => {
|
|
12
|
+
const text = lines.join("\n").trim();
|
|
13
|
+
if (text) {
|
|
14
|
+
sections.push({ heading, anchor: slugify(heading), text });
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
for (const line of markdown.split("\n")) {
|
|
18
|
+
if (FENCE.test(line)) {
|
|
19
|
+
inFence = !inFence;
|
|
20
|
+
}
|
|
21
|
+
const nextHeading = inFence ? undefined : HEADING.exec(line)?.[1]?.trim();
|
|
22
|
+
if (nextHeading) {
|
|
23
|
+
flush();
|
|
24
|
+
heading = nextHeading;
|
|
25
|
+
lines = [];
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
lines.push(line);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
flush();
|
|
32
|
+
return sections;
|
|
33
|
+
}
|
|
34
|
+
/** Approximates the Docusaurus heading id so search hits can link to the section. */
|
|
35
|
+
export function slugify(heading) {
|
|
36
|
+
return heading
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.replace(/[^\p{L}\p{N}\s-]/gu, "")
|
|
39
|
+
.trim()
|
|
40
|
+
.replace(/\s+/g, "-");
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=markdown.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (C) 2023-2026 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)<lichtblick@bmwgroup.com>
|
|
2
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { resolve, sep } from "node:path";
|
|
5
|
+
const FETCH_TIMEOUT_MS = 10_000;
|
|
6
|
+
export function withTrailingSlash(value) {
|
|
7
|
+
return value.endsWith("/") ? value : `${value}/`;
|
|
8
|
+
}
|
|
9
|
+
/** `location` is either an http(s) URL or a local Docusaurus build directory. */
|
|
10
|
+
export function createDocSource(location) {
|
|
11
|
+
return /^https?:\/\//i.test(location)
|
|
12
|
+
? httpSource(new URL(withTrailingSlash(location)))
|
|
13
|
+
: fileSource(resolve(location));
|
|
14
|
+
}
|
|
15
|
+
function httpSource(base) {
|
|
16
|
+
return {
|
|
17
|
+
async read(relativePath) {
|
|
18
|
+
const url = new URL(relativePath, base);
|
|
19
|
+
if (url.origin !== base.origin ||
|
|
20
|
+
!url.pathname.startsWith(base.pathname)) {
|
|
21
|
+
throw new Error(`Path escapes the docs source: ${relativePath}`);
|
|
22
|
+
}
|
|
23
|
+
const response = await fetch(url, {
|
|
24
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error(`GET ${url.href} failed with HTTP ${response.status}`);
|
|
28
|
+
}
|
|
29
|
+
return await response.text();
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function fileSource(root) {
|
|
34
|
+
return {
|
|
35
|
+
async read(relativePath) {
|
|
36
|
+
const file = resolve(root, relativePath);
|
|
37
|
+
if (!file.startsWith(root + sep)) {
|
|
38
|
+
throw new Error(`Path escapes the docs source: ${relativePath}`);
|
|
39
|
+
}
|
|
40
|
+
return await readFile(file, "utf8");
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=source.js.map
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (C) 2023-2026 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)<lichtblick@bmwgroup.com>
|
|
2
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
import MiniSearch from "minisearch";
|
|
4
|
+
import { errorMessage, silentLogger } from "../logging.js";
|
|
5
|
+
import { parseLlmsTxt, toPagePath, trimSlashes, } from "./llms-txt.js";
|
|
6
|
+
import { splitSections } from "./markdown.js";
|
|
7
|
+
const SNIPPET_LENGTH = 280;
|
|
8
|
+
export class DocsStore {
|
|
9
|
+
#source;
|
|
10
|
+
#siteUrl;
|
|
11
|
+
#logger;
|
|
12
|
+
#entries;
|
|
13
|
+
#index;
|
|
14
|
+
#markdown = new Map();
|
|
15
|
+
constructor(source, siteUrl, logger = silentLogger) {
|
|
16
|
+
this.#source = source;
|
|
17
|
+
this.#siteUrl = siteUrl;
|
|
18
|
+
this.#logger = logger;
|
|
19
|
+
}
|
|
20
|
+
async list(section) {
|
|
21
|
+
const entries = [...(await this.#loadEntries()).values()];
|
|
22
|
+
return section
|
|
23
|
+
? entries.filter((entry) => entry.section.toLowerCase() === section.toLowerCase())
|
|
24
|
+
: entries;
|
|
25
|
+
}
|
|
26
|
+
/** Accepts a page path, a path with `.md`/anchor, or a full page URL. */
|
|
27
|
+
async fetch(pathOrUrl) {
|
|
28
|
+
const entry = (await this.#loadEntries()).get(this.#normalizePath(pathOrUrl));
|
|
29
|
+
return (entry && { ...entry, markdown: await this.#loadMarkdown(entry.path) });
|
|
30
|
+
}
|
|
31
|
+
async search(query, limit) {
|
|
32
|
+
const { search, sections } = await this.#loadIndex();
|
|
33
|
+
return search
|
|
34
|
+
.search(query)
|
|
35
|
+
.slice(0, limit)
|
|
36
|
+
.flatMap((result) => {
|
|
37
|
+
const section = sections.get(result.id);
|
|
38
|
+
if (!section) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const pageUrl = new URL(section.path, this.#siteUrl).href;
|
|
42
|
+
return [
|
|
43
|
+
{
|
|
44
|
+
path: section.path,
|
|
45
|
+
title: section.title,
|
|
46
|
+
heading: section.heading,
|
|
47
|
+
url: section.anchor ? `${pageUrl}#${section.anchor}` : pageUrl,
|
|
48
|
+
snippet: snippet(section.text, result.terms),
|
|
49
|
+
score: result.score,
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** Starts building the search index in the background so the first docs_search is fast. */
|
|
55
|
+
warmUp() {
|
|
56
|
+
this.#loadIndex().catch((error) => {
|
|
57
|
+
this.#logger("error", {
|
|
58
|
+
message: "failed to build the docs index",
|
|
59
|
+
error: errorMessage(error),
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/** Fetches every page listed in llms.txt and reports the ones that cannot be loaded. */
|
|
64
|
+
async verify() {
|
|
65
|
+
const { loaded, failures } = await this.#loadAllPages();
|
|
66
|
+
return { pages: loaded.length + failures.length, failures };
|
|
67
|
+
}
|
|
68
|
+
#normalizePath(pathOrUrl) {
|
|
69
|
+
if (/^https?:\/\//i.test(pathOrUrl)) {
|
|
70
|
+
return toPagePath(pathOrUrl, this.#siteUrl) ?? "";
|
|
71
|
+
}
|
|
72
|
+
const withoutAnchor = pathOrUrl.split("#")[0] ?? "";
|
|
73
|
+
return trimSlashes(withoutAnchor.endsWith(".md")
|
|
74
|
+
? withoutAnchor.slice(0, -3)
|
|
75
|
+
: withoutAnchor);
|
|
76
|
+
}
|
|
77
|
+
async #loadEntries() {
|
|
78
|
+
if (!this.#entries) {
|
|
79
|
+
this.#entries = this.#source
|
|
80
|
+
.read("llms.txt")
|
|
81
|
+
.then((text) => new Map(parseLlmsTxt(text, this.#siteUrl).map((entry) => [
|
|
82
|
+
entry.path,
|
|
83
|
+
entry,
|
|
84
|
+
])));
|
|
85
|
+
this.#entries.catch(() => (this.#entries = undefined));
|
|
86
|
+
}
|
|
87
|
+
return await this.#entries;
|
|
88
|
+
}
|
|
89
|
+
async #loadMarkdown(path) {
|
|
90
|
+
let markdown = this.#markdown.get(path);
|
|
91
|
+
if (!markdown) {
|
|
92
|
+
markdown = this.#source.read(`${path}.md`);
|
|
93
|
+
this.#markdown.set(path, markdown);
|
|
94
|
+
markdown.catch(() => this.#markdown.delete(path));
|
|
95
|
+
}
|
|
96
|
+
return await markdown;
|
|
97
|
+
}
|
|
98
|
+
async #loadIndex() {
|
|
99
|
+
if (!this.#index) {
|
|
100
|
+
this.#index = this.#buildIndex();
|
|
101
|
+
this.#index.catch(() => (this.#index = undefined));
|
|
102
|
+
}
|
|
103
|
+
return await this.#index;
|
|
104
|
+
}
|
|
105
|
+
async #loadAllPages() {
|
|
106
|
+
const entries = [...(await this.#loadEntries()).values()];
|
|
107
|
+
const results = await Promise.all(entries.map(async (entry) => {
|
|
108
|
+
try {
|
|
109
|
+
return { entry, markdown: await this.#loadMarkdown(entry.path) };
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
return { entry, error: errorMessage(error) };
|
|
113
|
+
}
|
|
114
|
+
}));
|
|
115
|
+
const pages = { loaded: [], failures: [] };
|
|
116
|
+
for (const { entry, markdown, error } of results) {
|
|
117
|
+
if (markdown != undefined) {
|
|
118
|
+
pages.loaded.push({ entry, markdown });
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
pages.failures.push({ path: entry.path, reason: error });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return pages;
|
|
125
|
+
}
|
|
126
|
+
async #buildIndex() {
|
|
127
|
+
const start = performance.now();
|
|
128
|
+
const { loaded, failures } = await this.#loadAllPages();
|
|
129
|
+
const sections = new Map();
|
|
130
|
+
for (const { entry, markdown } of loaded) {
|
|
131
|
+
splitSections(markdown).forEach((section, i) => {
|
|
132
|
+
const id = `${entry.path}::${i}`;
|
|
133
|
+
sections.set(id, {
|
|
134
|
+
id,
|
|
135
|
+
path: entry.path,
|
|
136
|
+
title: entry.title,
|
|
137
|
+
...section,
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const search = new MiniSearch({
|
|
142
|
+
fields: ["title", "heading", "text"],
|
|
143
|
+
searchOptions: {
|
|
144
|
+
boost: { title: 3, heading: 2 },
|
|
145
|
+
prefix: true,
|
|
146
|
+
fuzzy: 0.2,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
search.addAll([...sections.values()]);
|
|
150
|
+
this.#logger("docs_index", {
|
|
151
|
+
pages: loaded.length,
|
|
152
|
+
sections: sections.size,
|
|
153
|
+
durationMs: Math.round(performance.now() - start),
|
|
154
|
+
skipped: failures.map(({ path, reason }) => `${path} (${reason})`),
|
|
155
|
+
});
|
|
156
|
+
return { search, sections };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function snippet(text, terms) {
|
|
160
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
161
|
+
const lower = flat.toLowerCase();
|
|
162
|
+
const firstHit = Math.min(...terms
|
|
163
|
+
.map((term) => lower.indexOf(term.toLowerCase()))
|
|
164
|
+
.filter((index) => index >= 0));
|
|
165
|
+
const start = Number.isFinite(firstHit) ? Math.max(0, firstHit - 80) : 0;
|
|
166
|
+
const end = Math.min(flat.length, start + SNIPPET_LENGTH);
|
|
167
|
+
return `${start > 0 ? "…" : ""}${flat.slice(start, end)}${end < flat.length ? "…" : ""}`;
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=store.js.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-FileCopyrightText: Copyright (C) 2023-2026 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)<lichtblick@bmwgroup.com>
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { ExtensionApi } from "./api/extension-api.js";
|
|
6
|
+
import { runCheck } from "./check.js";
|
|
7
|
+
import { loadConfig } from "./config.js";
|
|
8
|
+
import { createDocSource } from "./docs/source.js";
|
|
9
|
+
import { DocsStore } from "./docs/store.js";
|
|
10
|
+
import { createLogger, errorMessage, silentLogger } from "./logging.js";
|
|
11
|
+
import { createServer, SERVER_VERSION } from "./server.js";
|
|
12
|
+
const config = loadConfig();
|
|
13
|
+
if (process.argv[2] === "check") {
|
|
14
|
+
process.exitCode = await runCheck(config);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
const logger = config.log ? createLogger() : silentLogger;
|
|
18
|
+
try {
|
|
19
|
+
const docs = new DocsStore(createDocSource(config.docsSource), config.siteUrl, logger);
|
|
20
|
+
const server = createServer({
|
|
21
|
+
docs,
|
|
22
|
+
api: new ExtensionApi(),
|
|
23
|
+
logger,
|
|
24
|
+
});
|
|
25
|
+
await server.connect(new StdioServerTransport());
|
|
26
|
+
logger("ready", { version: SERVER_VERSION, docsSource: config.docsSource });
|
|
27
|
+
docs.warmUp();
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
// Fatal errors are reported even when logging is disabled.
|
|
31
|
+
createLogger()("error", {
|
|
32
|
+
message: "failed to start lichtblick-docs-mcp",
|
|
33
|
+
error: errorMessage(error),
|
|
34
|
+
});
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=index.js.map
|
package/dist/logging.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (C) 2023-2026 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)<lichtblick@bmwgroup.com>
|
|
2
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
export const silentLogger = () => { };
|
|
4
|
+
/** Writes one JSON object per line, to stderr by default: stdout carries the MCP protocol. */
|
|
5
|
+
export function createLogger(write = (line) => void process.stderr.write(`${line}\n`)) {
|
|
6
|
+
return (event, fields) => {
|
|
7
|
+
write(JSON.stringify({ ts: new Date().toISOString(), event, ...fields }));
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function errorMessage(error) {
|
|
11
|
+
return error instanceof Error ? error.message : String(error);
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=logging.js.map
|