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.
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/index.js +429 -0
- 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
|
+
[](https://www.npmjs.com/package/openclaw-docs-mcp)
|
|
4
|
+
[](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
|
+
}
|