openclaw-docs-mcp 1.0.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -0
  3. package/index.js +429 -0
  4. package/package.json +46 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenClaw Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # openclaw-docs-mcp
2
+
3
+ [![npm version](https://badge.fury.io/js/openclaw-docs-mcp.svg)](https://www.npmjs.com/package/openclaw-docs-mcp)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ A Model Context Protocol (MCP) server for searching and retrieving documentation from [OpenClaw](https://docs.openclaw.ai/).
7
+
8
+ ## Features
9
+
10
+ - 🔍 **Search documentation** - Full-text search across all OpenClaw docs
11
+ - 📄 **Retrieve pages** - Get complete content of any documentation page
12
+ - 📋 **List all pages** - Browse the full documentation structure
13
+ - ⚡ **Fast caching** - Built-in caching for improved performance
14
+
15
+ ## Installation
16
+
17
+ ### Global Installation (recommended)
18
+
19
+ ```bash
20
+ npm install -g openclaw-docs-mcp
21
+ ```
22
+
23
+ ### Local Installation
24
+
25
+ ```bash
26
+ npm install openclaw-docs-mcp
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ### As MCP Server
32
+
33
+ Run directly:
34
+
35
+ ```bash
36
+ openclaw-docs-mcp
37
+ ```
38
+
39
+ Or with npx:
40
+
41
+ ```bash
42
+ npx openclaw-docs-mcp
43
+ ```
44
+
45
+ ### VS Code Configuration
46
+
47
+ Add to your VS Code `mcp.json` (`~/.config/Code/User/mcp.json` or `%APPDATA%\Code\User\mcp.json`):
48
+
49
+ ```json
50
+ {
51
+ "servers": {
52
+ "openclaw-docs": {
53
+ "type": "stdio",
54
+ "command": "npx",
55
+ "args": ["-y", "openclaw-docs-mcp"]
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ For WSL users on Windows:
62
+
63
+ ```json
64
+ {
65
+ "servers": {
66
+ "openclaw-docs": {
67
+ "type": "stdio",
68
+ "command": "wsl",
69
+ "args": ["-e", "npx", "-y", "openclaw-docs-mcp"]
70
+ }
71
+ }
72
+ }
73
+ ```
74
+
75
+ ### Claude Desktop Configuration
76
+
77
+ Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
78
+
79
+ ```json
80
+ {
81
+ "mcpServers": {
82
+ "openclaw-docs": {
83
+ "command": "npx",
84
+ "args": ["-y", "openclaw-docs-mcp"]
85
+ }
86
+ }
87
+ }
88
+ ```
89
+
90
+ ## Available Tools
91
+
92
+ ### search_openclaw_docs
93
+
94
+ Search OpenClaw documentation for information about a topic.
95
+
96
+ **Parameters:**
97
+ - `query` (string, required): Search query (e.g., "gateway configuration", "telegram channel")
98
+ - `maxResults` (number, optional): Maximum results to return (default: 5, max: 20)
99
+
100
+ **Example:**
101
+ ```json
102
+ {
103
+ "name": "search_openclaw_docs",
104
+ "arguments": {
105
+ "query": "telegram channel setup",
106
+ "maxResults": 5
107
+ }
108
+ }
109
+ ```
110
+
111
+ ### get_openclaw_doc_page
112
+
113
+ Get the full content of a specific documentation page.
114
+
115
+ **Parameters:**
116
+ - `path` (string, required): Page path (e.g., "/gateway/configuration", "/channels/telegram")
117
+
118
+ **Example:**
119
+ ```json
120
+ {
121
+ "name": "get_openclaw_doc_page",
122
+ "arguments": {
123
+ "path": "/gateway/configuration"
124
+ }
125
+ }
126
+ ```
127
+
128
+ ### list_openclaw_doc_pages
129
+
130
+ List all available documentation pages from the sitemap.
131
+
132
+ **Parameters:** None
133
+
134
+ **Example:**
135
+ ```json
136
+ {
137
+ "name": "list_openclaw_doc_pages",
138
+ "arguments": {}
139
+ }
140
+ ```
141
+
142
+ ## How It Works
143
+
144
+ 1. Fetches the sitemap from `https://docs.openclaw.ai/sitemap.xml`
145
+ 2. Parses HTML content using Cheerio to extract text
146
+ 3. Performs keyword-based relevance scoring for search results
147
+ 4. Caches results for 5 minutes to improve performance
148
+
149
+ ## Development
150
+
151
+ ```bash
152
+ # Clone the repository
153
+ git clone https://github.com/openclaw/openclaw-docs-mcp.git
154
+ cd openclaw-docs-mcp
155
+
156
+ # Install dependencies
157
+ npm install
158
+
159
+ # Run locally
160
+ node index.js
161
+
162
+ # Test tools/list
163
+ echo '{"jsonrpc": "2.0", "method": "tools/list", "id": 1}' | node index.js
164
+ ```
165
+
166
+ ## License
167
+
168
+ MIT License - see [LICENSE](LICENSE) for details.
169
+
170
+ ## Related
171
+
172
+ - [OpenClaw Documentation](https://docs.openclaw.ai/)
173
+ - [OpenClaw GitHub](https://github.com/openclaw/openclaw)
174
+ - [Model Context Protocol](https://modelcontextprotocol.io/)
package/index.js ADDED
@@ -0,0 +1,429 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * OpenClaw Docs MCP Server
5
+ *
6
+ * A Model Context Protocol server that provides tools to search and retrieve
7
+ * documentation from https://docs.openclaw.ai/
8
+ *
9
+ * Features:
10
+ * - Fetches sitemap to get all documentation URLs
11
+ * - Searches documentation content
12
+ * - Retrieves specific documentation pages
13
+ */
14
+
15
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
16
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
17
+ import {
18
+ CallToolRequestSchema,
19
+ ListToolsRequestSchema,
20
+ } from "@modelcontextprotocol/sdk/types.js";
21
+ import fetch from "node-fetch";
22
+ import { parseStringPromise } from "xml2js";
23
+ import * as cheerio from "cheerio";
24
+
25
+ const DOCS_BASE_URL = "https://docs.openclaw.ai";
26
+ const SITEMAP_URL = `${DOCS_BASE_URL}/sitemap.xml`;
27
+
28
+ // Cache for sitemap URLs and page content
29
+ let sitemapCache = null;
30
+ let sitemapCacheTime = 0;
31
+ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
32
+
33
+ // Content cache for faster repeated searches
34
+ const contentCache = new Map();
35
+
36
+ /**
37
+ * Fetch and parse the sitemap to get all documentation URLs
38
+ */
39
+ async function fetchSitemap() {
40
+ const now = Date.now();
41
+ if (sitemapCache && now - sitemapCacheTime < CACHE_TTL) {
42
+ return sitemapCache;
43
+ }
44
+
45
+ try {
46
+ const response = await fetch(SITEMAP_URL);
47
+ const xml = await response.text();
48
+ const result = await parseStringPromise(xml);
49
+
50
+ const urls = result.urlset.url.map((entry) => ({
51
+ loc: entry.loc[0],
52
+ lastmod: entry.lastmod?.[0] || null,
53
+ }));
54
+
55
+ sitemapCache = urls;
56
+ sitemapCacheTime = now;
57
+ return urls;
58
+ } catch (error) {
59
+ console.error("Error fetching sitemap:", error);
60
+ throw new Error(`Failed to fetch sitemap: ${error.message}`);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Fetch and parse a documentation page to extract text content
66
+ */
67
+ async function fetchPageContent(url) {
68
+ // Check cache first
69
+ if (contentCache.has(url)) {
70
+ return contentCache.get(url);
71
+ }
72
+
73
+ try {
74
+ const response = await fetch(url);
75
+ const html = await response.text();
76
+ const $ = cheerio.load(html);
77
+
78
+ // Remove script and style elements
79
+ $("script, style, nav, header, footer, .sidebar, .toc").remove();
80
+
81
+ // Try to find the main content area
82
+ let content = "";
83
+ const contentSelectors = [
84
+ "article",
85
+ ".markdown",
86
+ ".mdx-content",
87
+ "main",
88
+ "#content-container",
89
+ ".prose",
90
+ ];
91
+
92
+ for (const selector of contentSelectors) {
93
+ const el = $(selector);
94
+ if (el.length > 0) {
95
+ content = el.text();
96
+ break;
97
+ }
98
+ }
99
+
100
+ if (!content) {
101
+ content = $("body").text();
102
+ }
103
+
104
+ // Clean up whitespace
105
+ content = content
106
+ .replace(/\s+/g, " ")
107
+ .replace(/\n\s*\n/g, "\n")
108
+ .trim();
109
+
110
+ // Extract title
111
+ const title = $("h1").first().text().trim() ||
112
+ $("title").text().trim().replace(" | OpenClaw Docs", "") ||
113
+ url.split("/").pop();
114
+
115
+ // Extract description/summary (first paragraph or meta description)
116
+ const description = $('meta[name="description"]').attr("content") ||
117
+ $("p").first().text().trim().slice(0, 200);
118
+
119
+ const pageData = {
120
+ url,
121
+ title,
122
+ description,
123
+ content: content.slice(0, 50000), // Limit content size
124
+ fetchedAt: Date.now(),
125
+ };
126
+
127
+ // Cache the result
128
+ contentCache.set(url, pageData);
129
+
130
+ return pageData;
131
+ } catch (error) {
132
+ console.error(`Error fetching page ${url}:`, error);
133
+ throw new Error(`Failed to fetch page: ${error.message}`);
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Search documentation pages for a query
139
+ */
140
+ async function searchDocs(query, maxResults = 10) {
141
+ const urls = await fetchSitemap();
142
+ const queryLower = query.toLowerCase();
143
+ const queryTerms = queryLower.split(/\s+/).filter(t => t.length > 2);
144
+
145
+ const results = [];
146
+
147
+ // First, try to find matches based on URL/path
148
+ const urlMatches = urls.filter(({ loc }) => {
149
+ const path = loc.replace(DOCS_BASE_URL, "").toLowerCase();
150
+ return queryTerms.some(term => path.includes(term));
151
+ });
152
+
153
+ // Prioritize URL matches, then fetch content for the most relevant
154
+ const urlsToCheck = [
155
+ ...urlMatches.slice(0, Math.min(10, maxResults)),
156
+ ...urls.filter(u => !urlMatches.includes(u)).slice(0, 20),
157
+ ].slice(0, 30);
158
+
159
+ // Fetch and search content in parallel (limited concurrency)
160
+ const batchSize = 5;
161
+ for (let i = 0; i < urlsToCheck.length && results.length < maxResults; i += batchSize) {
162
+ const batch = urlsToCheck.slice(i, i + batchSize);
163
+ const pagePromises = batch.map(async ({ loc }) => {
164
+ try {
165
+ const page = await fetchPageContent(loc);
166
+ const contentLower = page.content.toLowerCase();
167
+ const titleLower = page.title.toLowerCase();
168
+
169
+ // Calculate relevance score
170
+ let score = 0;
171
+ for (const term of queryTerms) {
172
+ if (titleLower.includes(term)) score += 10;
173
+ if (page.url.toLowerCase().includes(term)) score += 5;
174
+
175
+ // Count occurrences in content
176
+ const regex = new RegExp(term, "gi");
177
+ const matches = page.content.match(regex);
178
+ if (matches) score += Math.min(matches.length, 10);
179
+ }
180
+
181
+ if (score > 0) {
182
+ return { ...page, score };
183
+ }
184
+ return null;
185
+ } catch {
186
+ return null;
187
+ }
188
+ });
189
+
190
+ const batchResults = await Promise.all(pagePromises);
191
+ for (const result of batchResults) {
192
+ if (result && results.length < maxResults) {
193
+ results.push(result);
194
+ }
195
+ }
196
+ }
197
+
198
+ // Sort by relevance score
199
+ results.sort((a, b) => b.score - a.score);
200
+
201
+ return results.slice(0, maxResults);
202
+ }
203
+
204
+ /**
205
+ * Get a specific documentation page by path
206
+ */
207
+ async function getDocPage(path) {
208
+ // Normalize path
209
+ if (!path.startsWith("/")) {
210
+ path = "/" + path;
211
+ }
212
+ if (path.endsWith("/")) {
213
+ path = path.slice(0, -1);
214
+ }
215
+
216
+ const url = `${DOCS_BASE_URL}${path}`;
217
+ return await fetchPageContent(url);
218
+ }
219
+
220
+ /**
221
+ * List all documentation pages from sitemap
222
+ */
223
+ async function listDocPages() {
224
+ const urls = await fetchSitemap();
225
+ return urls.map(({ loc, lastmod }) => ({
226
+ path: loc.replace(DOCS_BASE_URL, "") || "/",
227
+ url: loc,
228
+ lastmod,
229
+ }));
230
+ }
231
+
232
+ // Create the MCP server
233
+ const server = new Server(
234
+ {
235
+ name: "openclaw-docs-mcp",
236
+ version: "1.0.0",
237
+ },
238
+ {
239
+ capabilities: {
240
+ tools: {},
241
+ },
242
+ }
243
+ );
244
+
245
+ // Define available tools
246
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
247
+ return {
248
+ tools: [
249
+ {
250
+ name: "search_openclaw_docs",
251
+ description:
252
+ "Search OpenClaw documentation for information about a topic. Returns relevant documentation pages with their content. Use this to find information about OpenClaw features, configuration, CLI commands, channels, plugins, and more.",
253
+ inputSchema: {
254
+ type: "object",
255
+ properties: {
256
+ query: {
257
+ type: "string",
258
+ description:
259
+ "The search query. Can include multiple keywords like 'gateway configuration' or 'telegram channel setup'",
260
+ },
261
+ maxResults: {
262
+ type: "number",
263
+ description:
264
+ "Maximum number of results to return (default: 5, max: 20)",
265
+ },
266
+ },
267
+ required: ["query"],
268
+ },
269
+ },
270
+ {
271
+ name: "get_openclaw_doc_page",
272
+ description:
273
+ "Get the full content of a specific OpenClaw documentation page by its path. Use this when you know the exact page you want to read.",
274
+ inputSchema: {
275
+ type: "object",
276
+ properties: {
277
+ path: {
278
+ type: "string",
279
+ description:
280
+ "The documentation page path, e.g., '/gateway/configuration', '/channels/telegram', '/cli/setup'",
281
+ },
282
+ },
283
+ required: ["path"],
284
+ },
285
+ },
286
+ {
287
+ name: "list_openclaw_doc_pages",
288
+ description:
289
+ "List all available OpenClaw documentation pages. Returns the sitemap with page paths and last modified dates. Useful for exploring the documentation structure.",
290
+ inputSchema: {
291
+ type: "object",
292
+ properties: {},
293
+ required: [],
294
+ },
295
+ },
296
+ ],
297
+ };
298
+ });
299
+
300
+ // Handle tool calls
301
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
302
+ const { name, arguments: args } = request.params;
303
+
304
+ try {
305
+ switch (name) {
306
+ case "search_openclaw_docs": {
307
+ const query = args.query;
308
+ const maxResults = Math.min(args.maxResults || 5, 20);
309
+
310
+ const results = await searchDocs(query, maxResults);
311
+
312
+ if (results.length === 0) {
313
+ return {
314
+ content: [
315
+ {
316
+ type: "text",
317
+ text: `No documentation found for query: "${query}"\n\nTry different keywords or use list_openclaw_doc_pages to see available pages.`,
318
+ },
319
+ ],
320
+ };
321
+ }
322
+
323
+ const formattedResults = results.map((r, i) => {
324
+ // Extract relevant snippet around the first query match
325
+ const queryTerms = query.toLowerCase().split(/\s+/);
326
+ let snippet = r.description;
327
+
328
+ for (const term of queryTerms) {
329
+ const idx = r.content.toLowerCase().indexOf(term);
330
+ if (idx !== -1) {
331
+ const start = Math.max(0, idx - 100);
332
+ const end = Math.min(r.content.length, idx + 300);
333
+ snippet = (start > 0 ? "..." : "") +
334
+ r.content.slice(start, end) +
335
+ (end < r.content.length ? "..." : "");
336
+ break;
337
+ }
338
+ }
339
+
340
+ return `## ${i + 1}. ${r.title}\n**URL:** ${r.url}\n**Relevance Score:** ${r.score}\n\n${snippet}\n`;
341
+ });
342
+
343
+ return {
344
+ content: [
345
+ {
346
+ type: "text",
347
+ text: `# Search Results for "${query}"\n\nFound ${results.length} relevant pages:\n\n${formattedResults.join("\n---\n\n")}`,
348
+ },
349
+ ],
350
+ };
351
+ }
352
+
353
+ case "get_openclaw_doc_page": {
354
+ const path = args.path;
355
+ const page = await getDocPage(path);
356
+
357
+ return {
358
+ content: [
359
+ {
360
+ type: "text",
361
+ text: `# ${page.title}\n\n**URL:** ${page.url}\n\n---\n\n${page.content}`,
362
+ },
363
+ ],
364
+ };
365
+ }
366
+
367
+ case "list_openclaw_doc_pages": {
368
+ const pages = await listDocPages();
369
+
370
+ // Group by section
371
+ const sections = {};
372
+ for (const page of pages) {
373
+ const parts = page.path.split("/").filter(Boolean);
374
+ const section = parts[0] || "root";
375
+ if (!sections[section]) {
376
+ sections[section] = [];
377
+ }
378
+ sections[section].push(page);
379
+ }
380
+
381
+ let output = "# OpenClaw Documentation Pages\n\n";
382
+ output += `Total pages: ${pages.length}\n\n`;
383
+
384
+ for (const [section, sectionPages] of Object.entries(sections).sort()) {
385
+ output += `## ${section}\n`;
386
+ for (const page of sectionPages.sort((a, b) => a.path.localeCompare(b.path))) {
387
+ output += `- [${page.path || "/"}](${page.url})`;
388
+ if (page.lastmod) {
389
+ output += ` (${page.lastmod.split("T")[0]})`;
390
+ }
391
+ output += "\n";
392
+ }
393
+ output += "\n";
394
+ }
395
+
396
+ return {
397
+ content: [
398
+ {
399
+ type: "text",
400
+ text: output,
401
+ },
402
+ ],
403
+ };
404
+ }
405
+
406
+ default:
407
+ throw new Error(`Unknown tool: ${name}`);
408
+ }
409
+ } catch (error) {
410
+ return {
411
+ content: [
412
+ {
413
+ type: "text",
414
+ text: `Error: ${error.message}`,
415
+ },
416
+ ],
417
+ isError: true,
418
+ };
419
+ }
420
+ });
421
+
422
+ // Start the server
423
+ async function main() {
424
+ const transport = new StdioServerTransport();
425
+ await server.connect(transport);
426
+ console.error("OpenClaw Docs MCP Server running on stdio");
427
+ }
428
+
429
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "openclaw-docs-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for searching OpenClaw documentation at docs.openclaw.ai",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "openclaw-docs-mcp": "./index.js"
9
+ },
10
+ "files": [
11
+ "index.js",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "start": "node index.js"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "model-context-protocol",
21
+ "openclaw",
22
+ "documentation",
23
+ "search",
24
+ "ai",
25
+ "llm"
26
+ ],
27
+ "author": "OpenClaw Contributors",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/openclaw/openclaw-docs-mcp"
32
+ },
33
+ "homepage": "https://docs.openclaw.ai",
34
+ "bugs": {
35
+ "url": "https://github.com/openclaw/openclaw-docs-mcp/issues"
36
+ },
37
+ "engines": {
38
+ "node": ">=18.0.0"
39
+ },
40
+ "dependencies": {
41
+ "@modelcontextprotocol/sdk": "^1.0.0",
42
+ "cheerio": "^1.0.0",
43
+ "node-fetch": "^3.3.2",
44
+ "xml2js": "^0.6.2"
45
+ }
46
+ }