@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/dist/server.js ADDED
@@ -0,0 +1,174 @@
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 { McpServer, ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { createRequire } from "node:module";
5
+ import { z } from "zod";
6
+ import { errorMessage, silentLogger } from "./logging.js";
7
+ export const { version: SERVER_VERSION } = createRequire(import.meta.url)("../package.json");
8
+ export const INSTRUCTIONS = `Authoritative, up-to-date reference for Lichtblick (an open-source visualization and diagnosis tool for robotics data, forked from Foxglove Studio) and for its extension API, the npm package @lichtblick/suite.
9
+
10
+ When to use these tools:
11
+ - For ANY question about Lichtblick: panels (3D, Image, Plot, Raw Messages, Map, ...), data sources (ROS 1/2, MCAP, rosbridge, Foxglove WebSocket, remote files), layouts, settings, extensions, custom panels, message converters, topic aliases or camera models.
12
+ - Call docs_search or extension_api_get BEFORE using web search or answering from memory. Your training data may be outdated, and Foxglove Studio documentation and APIs differ from Lichtblick's.
13
+ - Only fall back to web search if these tools return nothing relevant, and say so.
14
+
15
+ How to use them:
16
+ 1. docs_search with a few keywords, then docs_fetch the most relevant page path to read it in full.
17
+ 2. When writing or reviewing extension or panel code, call extension_api_get for every @lichtblick/suite type you use (e.g. PanelExtensionContext, RenderState, ExtensionContext, SettingsTreeNode) and use the exact signatures it returns. Use extension_api_list if you do not know the symbol name.
18
+ 3. docs_list shows every available page when searching is not enough.
19
+
20
+ Always cite the documentation URL returned by the tools in your answer.`;
21
+ export function createServer({ docs, api, logger = silentLogger, }) {
22
+ const server = new McpServer({ name: "lichtblick-docs", version: SERVER_VERSION }, { instructions: INSTRUCTIONS });
23
+ const logged = (tool, handler) => async (args) => {
24
+ const start = performance.now();
25
+ const log = (fields) => {
26
+ logger("tool_call", {
27
+ tool,
28
+ args,
29
+ durationMs: Math.round(performance.now() - start),
30
+ ...fields,
31
+ });
32
+ };
33
+ try {
34
+ const result = await handler(args);
35
+ log({ isError: result.isError === true });
36
+ return result;
37
+ }
38
+ catch (error) {
39
+ log({ isError: true, error: errorMessage(error) });
40
+ throw error;
41
+ }
42
+ };
43
+ server.registerTool("docs_list", {
44
+ title: "List Lichtblick documentation pages",
45
+ description: "Lists every page of the official Lichtblick documentation and guides with its path and summary. Use it to discover what is documented when docs_search is not enough; then read a page with docs_fetch.",
46
+ inputSchema: {
47
+ section: z
48
+ .string()
49
+ .optional()
50
+ .describe('Only list pages of this section: "Documentation" or "Guides".'),
51
+ },
52
+ annotations: { readOnlyHint: true },
53
+ }, logged("docs_list", async ({ section }) => {
54
+ const pages = await docs.list(section);
55
+ if (pages.length === 0) {
56
+ return errorText(section ? `No pages in section "${section}".` : "No pages found.");
57
+ }
58
+ return text(pages
59
+ .map((page) => [
60
+ `- ${page.title} [${page.section}] path: ${page.path}`,
61
+ page.description,
62
+ ]
63
+ .filter(Boolean)
64
+ .join(" — "))
65
+ .join("\n"));
66
+ }));
67
+ server.registerTool("docs_search", {
68
+ title: "Search Lichtblick documentation",
69
+ description: "Full-text search over the official Lichtblick documentation and guides (panels, data sources, layouts, settings, extensions, custom panels, message converters). Use this BEFORE web search for any Lichtblick question. Returns matching sections with page path, URL and snippet; read the full page with docs_fetch.",
70
+ inputSchema: {
71
+ query: z
72
+ .string()
73
+ .min(1)
74
+ .describe("A few keywords, e.g. 'subscribe topics custom panel', 'mcap remote file', '3d panel transforms'."),
75
+ limit: z
76
+ .number()
77
+ .int()
78
+ .min(1)
79
+ .max(20)
80
+ .default(8)
81
+ .describe("Maximum number of sections to return."),
82
+ },
83
+ annotations: { readOnlyHint: true },
84
+ }, logged("docs_search", async ({ query, limit }) => {
85
+ const hits = await docs.search(query, limit);
86
+ if (hits.length === 0) {
87
+ return text(`No results for "${query}". Try other or fewer keywords, or docs_list to browse all pages.`);
88
+ }
89
+ return text(hits
90
+ .map((hit, i) => {
91
+ const title = [hit.title, hit.heading].filter(Boolean).join(" › ");
92
+ return `${i + 1}. ${title}\n path: ${hit.path}\n url: ${hit.url}\n ${hit.snippet}`;
93
+ })
94
+ .join("\n\n"));
95
+ }));
96
+ server.registerTool("docs_fetch", {
97
+ title: "Fetch a Lichtblick documentation page",
98
+ description: "Returns the full markdown of an official Lichtblick documentation page, by path (as returned by docs_search or docs_list) or by URL. Prefer this over fetching the website.",
99
+ inputSchema: {
100
+ path: z
101
+ .string()
102
+ .min(1)
103
+ .describe("Page path or URL, e.g. 'docs/extensions/extension-api/panel-extension-context'."),
104
+ },
105
+ annotations: { readOnlyHint: true },
106
+ }, logged("docs_fetch", async ({ path }) => {
107
+ const page = await docs.fetch(path);
108
+ if (!page) {
109
+ return errorText(`Unknown page "${path}". Use docs_search or docs_list to find valid paths.`);
110
+ }
111
+ return text(`Source: ${page.url}\n\n${page.markdown}`);
112
+ }));
113
+ server.registerTool("extension_api_list", {
114
+ title: "List @lichtblick/suite API symbols",
115
+ description: "Lists every type, interface and constant exported by @lichtblick/suite, the Lichtblick extension API. Use it to find the right symbol name, then call extension_api_get.",
116
+ inputSchema: {},
117
+ annotations: { readOnlyHint: true },
118
+ }, logged("extension_api_list", async () => {
119
+ const { version: suiteVersion, symbols } = await api.load();
120
+ const lines = [...symbols.values()].map((symbol) => `- ${symbol.name} (${symbol.kind})`);
121
+ return text(`@lichtblick/suite@${suiteVersion} exports ${symbols.size} symbols:\n${lines.join("\n")}`);
122
+ }));
123
+ server.registerTool("extension_api_get", {
124
+ title: "Get a @lichtblick/suite API symbol",
125
+ description: "Returns the exact TypeScript declaration and doc comments of a symbol exported by @lichtblick/suite, e.g. PanelExtensionContext, ExtensionContext, RenderState, SettingsTreeNode, MessageEvent. Always use it instead of memory when writing Lichtblick extension or panel code: the API differs from Foxglove Studio's.",
126
+ inputSchema: {
127
+ symbol: z
128
+ .string()
129
+ .min(1)
130
+ .describe("Exported symbol name (case-insensitive)."),
131
+ },
132
+ annotations: { readOnlyHint: true },
133
+ }, logged("extension_api_get", async ({ symbol }) => {
134
+ const { version: suiteVersion } = await api.load();
135
+ const found = await api.get(symbol);
136
+ if (!found) {
137
+ const suggestions = await api.suggest(symbol);
138
+ const hint = suggestions.length > 0
139
+ ? `Did you mean: ${suggestions.join(", ")}?`
140
+ : "Use extension_api_list to see all symbols.";
141
+ return errorText(`No symbol "${symbol}" is exported by @lichtblick/suite@${suiteVersion}. ${hint}`);
142
+ }
143
+ return text(`${found.name} (${found.kind}) from @lichtblick/suite@${suiteVersion}, src/${found.file}\n\n\`\`\`ts\n${found.source}\n\`\`\``);
144
+ }));
145
+ server.registerResource("doc-page", new ResourceTemplate("lichtblick-docs://{+path}", {
146
+ list: async () => ({
147
+ resources: (await docs.list()).map((page) => ({
148
+ uri: `lichtblick-docs://${page.path}`,
149
+ name: page.title,
150
+ description: page.description,
151
+ mimeType: "text/markdown",
152
+ })),
153
+ }),
154
+ }), { title: "Lichtblick documentation page", mimeType: "text/markdown" }, async (uri, { path }) => {
155
+ const pagePath = Array.isArray(path) ? path.join("/") : String(path);
156
+ const page = await docs.fetch(pagePath);
157
+ if (!page) {
158
+ throw new Error(`Unknown documentation page: ${pagePath}`);
159
+ }
160
+ return {
161
+ contents: [
162
+ { uri: uri.href, mimeType: "text/markdown", text: page.markdown },
163
+ ],
164
+ };
165
+ });
166
+ return server;
167
+ }
168
+ function text(value) {
169
+ return { content: [{ type: "text", text: value }] };
170
+ }
171
+ function errorText(value) {
172
+ return { ...text(value), isError: true };
173
+ }
174
+ //# sourceMappingURL=server.js.map