@docsector/docsector-reader 0.8.4 β 0.9.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/README.md +93 -0
- package/bin/docsector.js +10 -1
- package/docsector.config.js +6 -0
- package/package.json +1 -1
- package/src/components/DPageBar.vue +33 -2
- package/src/i18n/helpers.js +6 -2
- package/src/index.js +6 -1
- package/src/mcp/server.js +280 -0
- package/src/quasar.factory.js +86 -0
package/README.md
CHANGED
|
@@ -24,6 +24,9 @@ Transform Markdown content into beautiful, navigable documentation sites β wit
|
|
|
24
24
|
- π **View as Markdown** β Open any page as plain text by appending `.md` to the URL, with locale support (`?lang=`)
|
|
25
25
|
- π€ **Open in ChatGPT / Claude** β One-click links to open the current page directly in ChatGPT or Claude for Q&A
|
|
26
26
|
- π€ **LLM Bot Detection** β Automatically serves raw Markdown to known AI crawlers (GPTBot, ClaudeBot, PerplexityBot, GrokBot, and others)
|
|
27
|
+
- πΊοΈ **Sitemap Generation** β Automatic `sitemap.xml` generation at build time with all page URLs (requires `siteUrl` in config)
|
|
28
|
+
- π€ **AI-Friendly robots.txt** β Scaffold includes a `robots.txt` explicitly allowing 23 AI crawlers (GPTBot, ClaudeBot, PerplexityBot, GrokBot, etc.)
|
|
29
|
+
- π **MCP Server** β Auto-generated [MCP](https://modelcontextprotocol.io) server at `/mcp` for AI assistant integration (Claude Desktop, VS Code, etc.)
|
|
27
30
|
|
|
28
31
|
---
|
|
29
32
|
|
|
@@ -45,6 +48,86 @@ Transform Markdown content into beautiful, navigable documentation sites β wit
|
|
|
45
48
|
|
|
46
49
|
---
|
|
47
50
|
|
|
51
|
+
## π MCP Server (Model Context Protocol)
|
|
52
|
+
|
|
53
|
+
Docsector Reader can automatically generate an [MCP](https://modelcontextprotocol.io) server at `/mcp` during build, allowing AI assistants like Claude to search and read your documentation in real time.
|
|
54
|
+
|
|
55
|
+
### Enable MCP
|
|
56
|
+
|
|
57
|
+
Add `mcp` to your `docsector.config.js`:
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
export default {
|
|
61
|
+
// ... other config ...
|
|
62
|
+
|
|
63
|
+
mcp: {
|
|
64
|
+
serverName: 'my-docs', // MCP server identifier
|
|
65
|
+
toolSuffix: 'my_docs' // Tool name suffix (e.g. search_my_docs)
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
siteUrl: 'https://my-docs.example.com' // Required for MCP URLs
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### What the build generates
|
|
73
|
+
|
|
74
|
+
When `mcp` is configured, `docsector build` generates:
|
|
75
|
+
|
|
76
|
+
| File | Purpose |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `dist/spa/mcp-pages.json` | Page index (title, path, type) for search |
|
|
79
|
+
| `functions/mcp.js` | Cloudflare Pages Function implementing MCP |
|
|
80
|
+
| `dist/spa/_routes.json` | Routes `/mcp` to the function |
|
|
81
|
+
| `dist/spa/_headers` | CORS headers for MCP endpoint |
|
|
82
|
+
|
|
83
|
+
### Exposed tools
|
|
84
|
+
|
|
85
|
+
| Tool | Description |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `search_{suffix}` | Search documentation by keyword, returns matching pages |
|
|
88
|
+
| `get_page_{suffix}` | Get full Markdown content of a specific page |
|
|
89
|
+
|
|
90
|
+
### Test locally
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npx docsector build
|
|
94
|
+
npx wrangler pages dev dist/spa
|
|
95
|
+
|
|
96
|
+
# In another terminal:
|
|
97
|
+
curl http://localhost:8788/mcp
|
|
98
|
+
curl -X POST http://localhost:8788/mcp \
|
|
99
|
+
-H 'Content-Type: application/json' \
|
|
100
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Configure in AI assistants
|
|
104
|
+
|
|
105
|
+
**VS Code** (`mcp.json`):
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"servers": {
|
|
109
|
+
"my-docs": {
|
|
110
|
+
"type": "http",
|
|
111
|
+
"url": "https://my-docs.example.com/mcp"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Claude Desktop** (`claude_desktop_config.json`):
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"mcpServers": {
|
|
121
|
+
"my-docs": {
|
|
122
|
+
"type": "url",
|
|
123
|
+
"url": "https://my-docs.example.com/mcp"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
48
131
|
## π Quick Start
|
|
49
132
|
|
|
50
133
|
### π¦ Install
|
|
@@ -189,6 +272,16 @@ export default {
|
|
|
189
272
|
}
|
|
190
273
|
```
|
|
191
274
|
|
|
275
|
+
### MCP (optional)
|
|
276
|
+
|
|
277
|
+
```javascript
|
|
278
|
+
// Enable MCP server at /mcp
|
|
279
|
+
mcp: {
|
|
280
|
+
serverName: 'my-project', // Server identifier
|
|
281
|
+
toolSuffix: 'my_project' // Tool name suffix
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
192
285
|
---
|
|
193
286
|
|
|
194
287
|
## π Internationalization
|
package/bin/docsector.js
CHANGED
|
@@ -23,7 +23,7 @@ const packageRoot = resolve(__dirname, '..')
|
|
|
23
23
|
const args = process.argv.slice(2)
|
|
24
24
|
const command = args[0]
|
|
25
25
|
|
|
26
|
-
const VERSION = '0.
|
|
26
|
+
const VERSION = '0.9.0'
|
|
27
27
|
|
|
28
28
|
const HELP = `
|
|
29
29
|
Docsector Reader v${VERSION}
|
|
@@ -125,6 +125,14 @@ export default {
|
|
|
125
125
|
editBaseUrl: 'https://github.com/your-org/your-repo/edit/main/src/pages'
|
|
126
126
|
},
|
|
127
127
|
|
|
128
|
+
// @ MCP (Model Context Protocol)
|
|
129
|
+
// Uncomment to enable an MCP server at /mcp for AI assistant integration.
|
|
130
|
+
// Requires Cloudflare Pages Functions (or compatible serverless platform).
|
|
131
|
+
// mcp: {
|
|
132
|
+
// serverName: 'my-docs',
|
|
133
|
+
// toolSuffix: 'my_docs'
|
|
134
|
+
// },
|
|
135
|
+
|
|
128
136
|
// @ Languages
|
|
129
137
|
languages: [
|
|
130
138
|
{
|
|
@@ -681,6 +689,7 @@ const TEMPLATE_GITIGNORE = `\
|
|
|
681
689
|
node_modules
|
|
682
690
|
.quasar
|
|
683
691
|
dist
|
|
692
|
+
functions
|
|
684
693
|
npm-debug.log*
|
|
685
694
|
.DS_Store
|
|
686
695
|
.thumbs.db
|
package/docsector.config.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docsector/docsector-reader",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "A documentation rendering engine built with Vue 3, Quasar v2 and Vite. Transform Markdown into beautiful, navigable documentation sites.",
|
|
5
5
|
"productName": "Docsector Reader",
|
|
6
6
|
"author": "Rodrigo de Araujo Vieira",
|
|
@@ -5,6 +5,7 @@ import { useStore } from 'vuex'
|
|
|
5
5
|
import { useI18n } from 'vue-i18n'
|
|
6
6
|
import { copyToClipboard, useQuasar } from 'quasar'
|
|
7
7
|
|
|
8
|
+
import docsectorConfig from 'docsector.config.js'
|
|
8
9
|
import gitDates from 'virtual:docsector-git-dates'
|
|
9
10
|
|
|
10
11
|
const $q = useQuasar()
|
|
@@ -17,10 +18,12 @@ const copied = ref(false)
|
|
|
17
18
|
const OPENAI_PATH = 'M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855l-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023l-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135l-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08L8.704 5.46a.795.795 0 0 0-.393.681zm1.097-2.365l2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5z'
|
|
18
19
|
const CLAUDE_PATH = 'M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z'
|
|
19
20
|
|
|
20
|
-
function buildIconURI (
|
|
21
|
+
function buildIconURI (paths, fillRule) {
|
|
21
22
|
const fill = $q.dark.isActive ? '#ccc' : '#555'
|
|
22
23
|
const fr = fillRule ? ` fill-rule="${fillRule}"` : ''
|
|
23
|
-
const
|
|
24
|
+
const pathArr = Array.isArray(paths) ? paths : [paths]
|
|
25
|
+
const pathEls = pathArr.map(d => `<path d="${d}"/>`).join('')
|
|
26
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" fill="${fill}"${fr} viewBox="0 0 24 24">${pathEls}</svg>`
|
|
24
27
|
return `img:data:image/svg+xml,${encodeURIComponent(svg)}`
|
|
25
28
|
}
|
|
26
29
|
|
|
@@ -85,6 +88,17 @@ const claudeURL = computed(() => {
|
|
|
85
88
|
return `https://claude.ai/new?q=${encodeURIComponent(prompt)}`
|
|
86
89
|
})
|
|
87
90
|
|
|
91
|
+
const MCP_PATHS = [
|
|
92
|
+
'M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z',
|
|
93
|
+
'M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z'
|
|
94
|
+
]
|
|
95
|
+
const mcpIcon = computed(() => buildIconURI(MCP_PATHS, 'evenodd'))
|
|
96
|
+
|
|
97
|
+
const mcpURL = computed(() => {
|
|
98
|
+
if (!docsectorConfig.mcp) return null
|
|
99
|
+
return `${window.location.origin}/mcp`
|
|
100
|
+
})
|
|
101
|
+
|
|
88
102
|
const copyPage = () => {
|
|
89
103
|
if (!rawMarkdown.value) return
|
|
90
104
|
|
|
@@ -163,6 +177,23 @@ const copyPage = () => {
|
|
|
163
177
|
<q-icon name="open_in_new" size="xs" />
|
|
164
178
|
</q-item-section>
|
|
165
179
|
</q-item>
|
|
180
|
+
|
|
181
|
+
<template v-if="mcpURL">
|
|
182
|
+
<q-separator />
|
|
183
|
+
|
|
184
|
+
<q-item clickable v-close-popup :href="mcpURL" target="_blank" class="q-py-sm">
|
|
185
|
+
<q-item-section avatar>
|
|
186
|
+
<q-icon :name="mcpIcon" size="xs" />
|
|
187
|
+
</q-item-section>
|
|
188
|
+
<q-item-section>
|
|
189
|
+
<q-item-label>{{ t('page.mcpServer') }}</q-item-label>
|
|
190
|
+
<q-item-label caption>{{ t('page.mcpServerCaption') }}</q-item-label>
|
|
191
|
+
</q-item-section>
|
|
192
|
+
<q-item-section side>
|
|
193
|
+
<q-icon name="open_in_new" size="xs" />
|
|
194
|
+
</q-item-section>
|
|
195
|
+
</q-item>
|
|
196
|
+
</template>
|
|
166
197
|
</q-list>
|
|
167
198
|
</q-btn-dropdown>
|
|
168
199
|
</div>
|
package/src/i18n/helpers.js
CHANGED
|
@@ -33,7 +33,9 @@ const engineDefaults = {
|
|
|
33
33
|
openInChatGPT: 'Open in ChatGPT',
|
|
34
34
|
openInChatGPTCaption: 'Ask ChatGPT about this page',
|
|
35
35
|
openInClaude: 'Open in Claude',
|
|
36
|
-
openInClaudeCaption: 'Ask Claude about this page'
|
|
36
|
+
openInClaudeCaption: 'Ask Claude about this page',
|
|
37
|
+
mcpServer: 'MCP Server',
|
|
38
|
+
mcpServerCaption: 'Connect AI assistants via MCP'
|
|
37
39
|
}
|
|
38
40
|
},
|
|
39
41
|
'pt-BR': {
|
|
@@ -47,7 +49,9 @@ const engineDefaults = {
|
|
|
47
49
|
openInChatGPT: 'Abrir no ChatGPT',
|
|
48
50
|
openInChatGPTCaption: 'Pergunte ao ChatGPT sobre esta pΓ‘gina',
|
|
49
51
|
openInClaude: 'Abrir no Claude',
|
|
50
|
-
openInClaudeCaption: 'Pergunte ao Claude sobre esta pΓ‘gina'
|
|
52
|
+
openInClaudeCaption: 'Pergunte ao Claude sobre esta pΓ‘gina',
|
|
53
|
+
mcpServer: 'Servidor MCP',
|
|
54
|
+
mcpServerCaption: 'Conecte assistentes de IA via MCP'
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
57
|
}
|
package/src/index.js
CHANGED
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
* @param {string} config.github.editBaseUrl - Base URL for "Edit on GitHub" links
|
|
41
41
|
* @param {Array} config.languages - Available languages [{image, label, value}]
|
|
42
42
|
* @param {string} [config.defaultLanguage='en-US'] - Default language code
|
|
43
|
+
* @param {Object} [config.mcp] - MCP (Model Context Protocol) server settings
|
|
44
|
+
* @param {string} config.mcp.serverName - Server name for MCP identification (e.g. 'my-docs')
|
|
45
|
+
* @param {string} config.mcp.toolSuffix - Suffix for tool names (e.g. 'my_docs' β search_my_docs)
|
|
43
46
|
* @returns {Object} Resolved Docsector configuration
|
|
44
47
|
*/
|
|
45
48
|
export function createDocsector (config = {}) {
|
|
@@ -77,7 +80,9 @@ export function createDocsector (config = {}) {
|
|
|
77
80
|
}
|
|
78
81
|
],
|
|
79
82
|
|
|
80
|
-
defaultLanguage: config.defaultLanguage || 'en-US'
|
|
83
|
+
defaultLanguage: config.defaultLanguage || 'en-US',
|
|
84
|
+
|
|
85
|
+
mcp: config.mcp || null
|
|
81
86
|
}
|
|
82
87
|
}
|
|
83
88
|
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docsector MCP Server β Cloudflare Pages Function
|
|
3
|
+
*
|
|
4
|
+
* Auto-generated by docsector build. Do not edit manually.
|
|
5
|
+
*
|
|
6
|
+
* This implements the MCP (Model Context Protocol) Streamable HTTP transport,
|
|
7
|
+
* exposing documentation content as tools for AI assistants.
|
|
8
|
+
*
|
|
9
|
+
* Placeholders replaced at build time:
|
|
10
|
+
* __MCP_SERVER_NAME__ β config.mcp.serverName
|
|
11
|
+
* __MCP_SERVER_VERSION__ β config.branding.version
|
|
12
|
+
* __MCP_TOOL_SUFFIX__ β config.mcp.toolSuffix
|
|
13
|
+
* __MCP_SITE_URL__ β config.siteUrl
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const SERVER_NAME = '__MCP_SERVER_NAME__'
|
|
17
|
+
const SERVER_VERSION = '__MCP_SERVER_VERSION__'
|
|
18
|
+
const TOOL_SUFFIX = '__MCP_TOOL_SUFFIX__'
|
|
19
|
+
const SITE_URL = '__MCP_SITE_URL__'
|
|
20
|
+
|
|
21
|
+
const PROTOCOL_VERSION = '2025-03-26'
|
|
22
|
+
|
|
23
|
+
const CORS_HEADERS = {
|
|
24
|
+
'Access-Control-Allow-Origin': '*',
|
|
25
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
26
|
+
'Access-Control-Allow-Headers': 'Content-Type, Accept, Mcp-Session-Id',
|
|
27
|
+
'Access-Control-Expose-Headers': 'Mcp-Session-Id'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function jsonResponse (body, status = 200) {
|
|
31
|
+
return new Response(JSON.stringify(body), {
|
|
32
|
+
status,
|
|
33
|
+
headers: { 'Content-Type': 'application/json', ...CORS_HEADERS }
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function jsonRpcResponse (id, result) {
|
|
38
|
+
return jsonResponse({ jsonrpc: '2.0', id, result })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function jsonRpcError (id, code, message) {
|
|
42
|
+
return jsonResponse({ jsonrpc: '2.0', id, error: { code, message } })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// --- Tool definitions ---
|
|
46
|
+
|
|
47
|
+
const TOOLS = [
|
|
48
|
+
{
|
|
49
|
+
name: `search_${TOOL_SUFFIX}`,
|
|
50
|
+
description: `Search the ${SERVER_NAME} documentation. Returns matching pages with titles and URLs. Use this to find relevant documentation pages by keyword.`,
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
query: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
description: 'Search term or phrase to find in the documentation'
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
required: ['query']
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: `get_page_${TOOL_SUFFIX}`,
|
|
64
|
+
description: `Get the full Markdown content of a specific ${SERVER_NAME} documentation page. Use the path returned by search_${TOOL_SUFFIX}, or provide a known documentation path.`,
|
|
65
|
+
inputSchema: {
|
|
66
|
+
type: 'object',
|
|
67
|
+
properties: {
|
|
68
|
+
path: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
description: 'Page path (e.g. "manual/CLI/Terminal/Output/overview"). Do not include leading slash or .md extension.'
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
required: ['path']
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
// --- Page index cache (per-isolate) ---
|
|
79
|
+
|
|
80
|
+
let cachedPages = null
|
|
81
|
+
|
|
82
|
+
async function getPages (env) {
|
|
83
|
+
if (cachedPages) return cachedPages
|
|
84
|
+
|
|
85
|
+
const url = new URL('/mcp-pages.json', SITE_URL || 'http://localhost')
|
|
86
|
+
const res = await env.ASSETS.fetch(url.toString())
|
|
87
|
+
|
|
88
|
+
if (!res.ok) return []
|
|
89
|
+
|
|
90
|
+
cachedPages = await res.json()
|
|
91
|
+
return cachedPages
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- Tool handlers ---
|
|
95
|
+
|
|
96
|
+
async function handleSearch (args, env) {
|
|
97
|
+
const query = (args.query || '').toLowerCase().trim()
|
|
98
|
+
if (!query) {
|
|
99
|
+
return { content: [{ type: 'text', text: 'Please provide a search query.' }], isError: true }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const pages = await getPages(env)
|
|
103
|
+
const terms = query.split(/\s+/)
|
|
104
|
+
|
|
105
|
+
// Score pages by title match
|
|
106
|
+
const results = []
|
|
107
|
+
for (const page of pages) {
|
|
108
|
+
const title = (page.title || '').toLowerCase()
|
|
109
|
+
const path = (page.path || '').toLowerCase()
|
|
110
|
+
|
|
111
|
+
let score = 0
|
|
112
|
+
for (const term of terms) {
|
|
113
|
+
if (title.includes(term)) score += 2
|
|
114
|
+
if (path.includes(term)) score += 1
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (score > 0) {
|
|
118
|
+
results.push({ ...page, score })
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
results.sort((a, b) => b.score - a.score)
|
|
123
|
+
const top = results.slice(0, 15)
|
|
124
|
+
|
|
125
|
+
if (top.length === 0) {
|
|
126
|
+
return {
|
|
127
|
+
content: [{ type: 'text', text: `No documentation pages found for "${args.query}".` }]
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const siteBase = (SITE_URL || '').replace(/\/+$/, '')
|
|
132
|
+
const lines = top.map(r => {
|
|
133
|
+
const url = siteBase ? `${siteBase}/${r.path}` : r.path
|
|
134
|
+
return `- **${r.title}** β [${r.path}](${url})`
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
content: [{
|
|
139
|
+
type: 'text',
|
|
140
|
+
text: `Found ${results.length} result(s) for "${args.query}":\n\n${lines.join('\n')}\n\nUse get_page_${TOOL_SUFFIX} with the path to read the full content.`
|
|
141
|
+
}]
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function handleGetPage (args, env) {
|
|
146
|
+
let path = (args.path || '').replace(/^\/+/, '').replace(/\/+$/, '').replace(/\.md$/, '')
|
|
147
|
+
if (!path) {
|
|
148
|
+
return { content: [{ type: 'text', text: 'Please provide a page path.' }], isError: true }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const siteBase = SITE_URL || 'http://localhost'
|
|
152
|
+
|
|
153
|
+
// Try the path as-is, then with /overview appended
|
|
154
|
+
const attempts = [`/${path}.md`]
|
|
155
|
+
if (!path.endsWith('/overview') && !path.endsWith('/showcase') && !path.endsWith('/vs')) {
|
|
156
|
+
attempts.push(`/${path}/overview.md`)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const mdPath of attempts) {
|
|
160
|
+
const url = new URL(mdPath, siteBase)
|
|
161
|
+
const res = await env.ASSETS.fetch(url.toString())
|
|
162
|
+
|
|
163
|
+
if (res.ok) {
|
|
164
|
+
const text = await res.text()
|
|
165
|
+
const siteUrl = siteBase.replace(/\/+$/, '')
|
|
166
|
+
const pagePath = mdPath.replace(/^\//, '').replace(/\.md$/, '')
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
content: [{
|
|
170
|
+
type: 'text',
|
|
171
|
+
text: `# ${pagePath}\n\nSource: ${siteUrl}/${pagePath}\n\n---\n\n${text}`
|
|
172
|
+
}]
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
content: [{
|
|
179
|
+
type: 'text',
|
|
180
|
+
text: `Page not found: "${path}". Use search_${TOOL_SUFFIX} to find available pages.`
|
|
181
|
+
}],
|
|
182
|
+
isError: true
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// --- MCP JSON-RPC dispatcher ---
|
|
187
|
+
|
|
188
|
+
async function handleJsonRpc (body, env) {
|
|
189
|
+
const { jsonrpc, id, method, params } = body
|
|
190
|
+
|
|
191
|
+
if (jsonrpc !== '2.0') {
|
|
192
|
+
return jsonRpcError(id ?? null, -32600, 'Invalid JSON-RPC version')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
switch (method) {
|
|
196
|
+
case 'initialize':
|
|
197
|
+
return jsonRpcResponse(id, {
|
|
198
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
199
|
+
capabilities: {
|
|
200
|
+
tools: { listChanged: false }
|
|
201
|
+
},
|
|
202
|
+
serverInfo: {
|
|
203
|
+
name: SERVER_NAME,
|
|
204
|
+
version: SERVER_VERSION
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
case 'notifications/initialized':
|
|
209
|
+
// Client acknowledgement β no response needed for notifications
|
|
210
|
+
return new Response(null, { status: 204, headers: CORS_HEADERS })
|
|
211
|
+
|
|
212
|
+
case 'tools/list':
|
|
213
|
+
return jsonRpcResponse(id, { tools: TOOLS })
|
|
214
|
+
|
|
215
|
+
case 'tools/call': {
|
|
216
|
+
const toolName = params?.name
|
|
217
|
+
const args = params?.arguments || {}
|
|
218
|
+
|
|
219
|
+
if (toolName === `search_${TOOL_SUFFIX}`) {
|
|
220
|
+
const result = await handleSearch(args, env)
|
|
221
|
+
return jsonRpcResponse(id, result)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (toolName === `get_page_${TOOL_SUFFIX}`) {
|
|
225
|
+
const result = await handleGetPage(args, env)
|
|
226
|
+
return jsonRpcResponse(id, result)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return jsonRpcError(id, -32602, `Unknown tool: "${toolName}"`)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
default:
|
|
233
|
+
return jsonRpcError(id, -32601, `Method not found: "${method}"`)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// --- Cloudflare Pages Function handlers ---
|
|
238
|
+
|
|
239
|
+
export async function onRequestGet (context) {
|
|
240
|
+
const siteBase = (SITE_URL || '').replace(/\/+$/, '')
|
|
241
|
+
|
|
242
|
+
return jsonResponse({
|
|
243
|
+
name: SERVER_NAME,
|
|
244
|
+
version: SERVER_VERSION,
|
|
245
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
246
|
+
capabilities: { tools: TOOLS.map(t => t.name) },
|
|
247
|
+
documentation: siteBase || undefined,
|
|
248
|
+
usage: {
|
|
249
|
+
claude_desktop: {
|
|
250
|
+
[SERVER_NAME]: { type: 'url', url: `${siteBase}/mcp` }
|
|
251
|
+
},
|
|
252
|
+
vscode: {
|
|
253
|
+
[SERVER_NAME]: { type: 'http', url: `${siteBase}/mcp` }
|
|
254
|
+
},
|
|
255
|
+
curl: `curl -X POST ${siteBase}/mcp -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'`
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function onRequestPost (context) {
|
|
261
|
+
const { request, env } = context
|
|
262
|
+
|
|
263
|
+
const contentType = request.headers.get('content-type') || ''
|
|
264
|
+
if (!contentType.includes('application/json')) {
|
|
265
|
+
return jsonRpcError(null, -32700, 'Content-Type must be application/json')
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
let body
|
|
269
|
+
try {
|
|
270
|
+
body = await request.json()
|
|
271
|
+
} catch {
|
|
272
|
+
return jsonRpcError(null, -32700, 'Parse error: invalid JSON')
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return handleJsonRpc(body, env)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export async function onRequestOptions () {
|
|
279
|
+
return new Response(null, { status: 204, headers: CORS_HEADERS })
|
|
280
|
+
}
|
package/src/quasar.factory.js
CHANGED
|
@@ -481,6 +481,92 @@ function createMarkdownBuildPlugin (projectRoot) {
|
|
|
481
481
|
writeFileSync(headersPath, headersRule)
|
|
482
482
|
}
|
|
483
483
|
console.log(`\x1b[36m[docsector]\x1b[0m Added _headers rule for .md files`)
|
|
484
|
+
|
|
485
|
+
// Generate MCP server if configured
|
|
486
|
+
if (config.mcp) {
|
|
487
|
+
const mcpConfig = config.mcp
|
|
488
|
+
const mcpServerName = mcpConfig.serverName || 'docs'
|
|
489
|
+
const mcpToolSuffix = mcpConfig.toolSuffix || 'docs'
|
|
490
|
+
const mcpVersion = config.branding?.version || '1.0.0'
|
|
491
|
+
|
|
492
|
+
// Collect page index for MCP
|
|
493
|
+
const mcpPages = []
|
|
494
|
+
for (const [pagePath, page] of Object.entries(pages)) {
|
|
495
|
+
if (page.config === null) continue
|
|
496
|
+
if (page.config.status === 'empty') continue
|
|
497
|
+
|
|
498
|
+
const type = page.config.type ?? 'manual'
|
|
499
|
+
const defaultTitle = page.data?.['*']?.title
|
|
500
|
+
|| page.data?.[defaultLang]?.title
|
|
501
|
+
|| page.data?.['en-US']?.title
|
|
502
|
+
|| pagePath.split('/').pop()
|
|
503
|
+
|| pagePath
|
|
504
|
+
|
|
505
|
+
const subpageList = ['overview']
|
|
506
|
+
if (page.config.subpages?.showcase) subpageList.push('showcase')
|
|
507
|
+
if (page.config.subpages?.vs) subpageList.push('vs')
|
|
508
|
+
|
|
509
|
+
for (const subpage of subpageList) {
|
|
510
|
+
const srcFile = resolve(pagesDir, `${type}${pagePath}.${subpage}.${defaultLang}.md`)
|
|
511
|
+
if (!existsSync(srcFile)) continue
|
|
512
|
+
|
|
513
|
+
mcpPages.push({
|
|
514
|
+
path: `${type}${pagePath}/${subpage}`,
|
|
515
|
+
title: defaultTitle,
|
|
516
|
+
type,
|
|
517
|
+
subpage
|
|
518
|
+
})
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Write mcp-pages.json
|
|
523
|
+
writeFileSync(
|
|
524
|
+
resolve(distDir, 'mcp-pages.json'),
|
|
525
|
+
JSON.stringify(mcpPages, null, 2)
|
|
526
|
+
)
|
|
527
|
+
console.log(`\x1b[36m[docsector]\x1b[0m Generated mcp-pages.json (${mcpPages.length} pages)`)
|
|
528
|
+
|
|
529
|
+
// Read server template from package, replace placeholders, write to project root functions/
|
|
530
|
+
// Cloudflare Pages expects functions/ in the project root, not inside dist/spa/
|
|
531
|
+
const packageRoot = getPackageRoot(projectRoot)
|
|
532
|
+
const templatePath = resolve(packageRoot, 'src', 'mcp', 'server.js')
|
|
533
|
+
if (existsSync(templatePath)) {
|
|
534
|
+
let serverCode = readFileSync(templatePath, 'utf-8')
|
|
535
|
+
serverCode = serverCode
|
|
536
|
+
.replaceAll('__MCP_SERVER_NAME__', mcpServerName)
|
|
537
|
+
.replaceAll('__MCP_SERVER_VERSION__', mcpVersion)
|
|
538
|
+
.replaceAll('__MCP_TOOL_SUFFIX__', mcpToolSuffix)
|
|
539
|
+
.replaceAll('__MCP_SITE_URL__', siteUrl || '')
|
|
540
|
+
|
|
541
|
+
const functionsDir = resolve(projectRoot, 'functions')
|
|
542
|
+
mkdirSync(functionsDir, { recursive: true })
|
|
543
|
+
writeFileSync(resolve(functionsDir, 'mcp.js'), serverCode)
|
|
544
|
+
console.log(`\x1b[36m[docsector]\x1b[0m Generated MCP server at functions/mcp.js`)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Add CORS headers for /mcp to _headers
|
|
548
|
+
const mcpHeaders = '/mcp\n Access-Control-Allow-Origin: *\n Access-Control-Allow-Methods: GET, POST, OPTIONS\n Access-Control-Allow-Headers: Content-Type, Accept, Mcp-Session-Id\n Access-Control-Expose-Headers: Mcp-Session-Id\n'
|
|
549
|
+
const currentHeaders = readFileSync(headersPath, 'utf-8')
|
|
550
|
+
if (!currentHeaders.includes('/mcp')) {
|
|
551
|
+
writeFileSync(headersPath, currentHeaders.trimEnd() + '\n\n' + mcpHeaders)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Generate or merge _routes.json for Cloudflare Pages
|
|
555
|
+
const routesPath = resolve(distDir, '_routes.json')
|
|
556
|
+
let routes = { version: 1, include: [], exclude: [] }
|
|
557
|
+
if (existsSync(routesPath)) {
|
|
558
|
+
try {
|
|
559
|
+
routes = JSON.parse(readFileSync(routesPath, 'utf-8'))
|
|
560
|
+
} catch {
|
|
561
|
+
// empty
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (!routes.include.includes('/mcp')) {
|
|
565
|
+
routes.include.push('/mcp')
|
|
566
|
+
}
|
|
567
|
+
writeFileSync(routesPath, JSON.stringify(routes, null, 2))
|
|
568
|
+
console.log(`\x1b[36m[docsector]\x1b[0m Added /mcp to _routes.json`)
|
|
569
|
+
}
|
|
484
570
|
}
|
|
485
571
|
}
|
|
486
572
|
}
|