@docsector/docsector-reader 0.8.4 β 0.9.1
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 +111 -2
- package/src/i18n/helpers.js +18 -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.1'
|
|
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.1",
|
|
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,56 @@ 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 VSCODE_PATH = 'M70.9119 99.3171C72.4869 99.9307 74.2828 99.8914 75.8725 99.1264L96.4608 89.2197C98.6242 88.1787 100 85.9892 100 83.5872V16.4133C100 14.0113 98.6243 11.8218 96.4609 10.7808L75.8725 0.873756C73.7862 -0.130129 71.3446 0.11576 69.5135 1.44695C69.252 1.63711 69.0028 1.84943 68.769 2.08341L29.3551 38.0415L12.1872 25.0096C10.589 23.7965 8.35363 23.8959 6.86933 25.2461L1.36303 30.2549C-0.452552 31.9064 -0.454633 34.7627 1.35853 36.417L16.2471 50.0001L1.35853 63.5832C-0.454633 65.2374 -0.452552 68.0938 1.36303 69.7453L6.86933 74.7541C8.35363 76.1043 10.589 76.2037 12.1872 74.9905L29.3551 61.9587L68.769 97.9167C69.3925 98.5406 70.1246 99.0104 70.9119 99.3171ZM75.0152 27.2989L45.1091 50.0001L75.0152 72.7012V27.2989Z'
|
|
96
|
+
const CODEX_PATH = 'M15.672 11.249a.75.75 0 00-.006-1.5 3.504 3.504 0 01-3.26-2.26.75.75 0 00-1.392 0 3.504 3.504 0 01-3.26 2.26.75.75 0 000 1.5 3.504 3.504 0 013.258 2.252.75.75 0 001.396-.004A3.504 3.504 0 0115.672 11.249zM21.665 7.317a.75.75 0 000-1.5 5.253 5.253 0 01-4.887-3.386.75.75 0 00-1.392 0A5.253 5.253 0 0110.5 5.817a.75.75 0 000 1.5 5.253 5.253 0 014.886 3.386.75.75 0 001.392 0 5.253 5.253 0 014.887-3.386z'
|
|
97
|
+
|
|
98
|
+
const mcpIcon = computed(() => buildIconURI(MCP_PATHS, 'evenodd'))
|
|
99
|
+
|
|
100
|
+
function buildVSCodeIconURI () {
|
|
101
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill-rule="evenodd" clip-rule="evenodd" d="${VSCODE_PATH}" fill="%23007ACC"/></svg>`
|
|
102
|
+
return `img:data:image/svg+xml,${svg}`
|
|
103
|
+
}
|
|
104
|
+
const vscodeIcon = computed(() => buildVSCodeIconURI())
|
|
105
|
+
const codexIcon = computed(() => buildIconURI(CODEX_PATH))
|
|
106
|
+
|
|
107
|
+
const mcpURL = computed(() => {
|
|
108
|
+
if (!docsectorConfig.mcp) return null
|
|
109
|
+
return `${window.location.origin}/mcp`
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const vscodeMcpURL = computed(() => {
|
|
113
|
+
if (!docsectorConfig.mcp) return null
|
|
114
|
+
const name = docsectorConfig.mcp.serverName
|
|
115
|
+
const url = `${window.location.origin}/mcp`
|
|
116
|
+
return `vscode:mcp/install?${encodeURIComponent(JSON.stringify({ name, url }))}`
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const claudeCodeCommand = computed(() => {
|
|
120
|
+
if (!docsectorConfig.mcp) return null
|
|
121
|
+
const name = docsectorConfig.mcp.serverName
|
|
122
|
+
const url = `${window.location.origin}/mcp`
|
|
123
|
+
return `claude mcp add ${name} --scope user --transport http ${url}`
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const codexCommand = computed(() => {
|
|
127
|
+
if (!docsectorConfig.mcp) return null
|
|
128
|
+
const name = docsectorConfig.mcp.serverName
|
|
129
|
+
const url = `${window.location.origin}/mcp`
|
|
130
|
+
return `codex mcp add ${name} --url ${url}`
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
const copiedMcp = ref(null)
|
|
134
|
+
const copyMcpCommand = (command, type) => {
|
|
135
|
+
copyToClipboard(command).then(() => {
|
|
136
|
+
copiedMcp.value = type
|
|
137
|
+
setTimeout(() => { copiedMcp.value = null }, 2000)
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
88
141
|
const copyPage = () => {
|
|
89
142
|
if (!rawMarkdown.value) return
|
|
90
143
|
|
|
@@ -163,6 +216,62 @@ const copyPage = () => {
|
|
|
163
216
|
<q-icon name="open_in_new" size="xs" />
|
|
164
217
|
</q-item-section>
|
|
165
218
|
</q-item>
|
|
219
|
+
|
|
220
|
+
<template v-if="mcpURL">
|
|
221
|
+
<q-separator />
|
|
222
|
+
|
|
223
|
+
<q-item clickable v-close-popup :href="mcpURL" target="_blank" class="q-py-sm">
|
|
224
|
+
<q-item-section avatar>
|
|
225
|
+
<q-icon :name="mcpIcon" size="xs" />
|
|
226
|
+
</q-item-section>
|
|
227
|
+
<q-item-section>
|
|
228
|
+
<q-item-label>{{ t('page.mcpServer') }}</q-item-label>
|
|
229
|
+
<q-item-label caption>{{ t('page.mcpServerCaption') }}</q-item-label>
|
|
230
|
+
</q-item-section>
|
|
231
|
+
<q-item-section side>
|
|
232
|
+
<q-icon name="open_in_new" size="xs" />
|
|
233
|
+
</q-item-section>
|
|
234
|
+
</q-item>
|
|
235
|
+
|
|
236
|
+
<q-item clickable v-close-popup :href="vscodeMcpURL" class="q-py-sm">
|
|
237
|
+
<q-item-section avatar>
|
|
238
|
+
<q-icon :name="vscodeIcon" size="xs" />
|
|
239
|
+
</q-item-section>
|
|
240
|
+
<q-item-section>
|
|
241
|
+
<q-item-label>{{ t('page.connectVSCode') }}</q-item-label>
|
|
242
|
+
<q-item-label caption>{{ t('page.connectVSCodeCaption') }}</q-item-label>
|
|
243
|
+
</q-item-section>
|
|
244
|
+
<q-item-section side>
|
|
245
|
+
<q-icon name="open_in_new" size="xs" />
|
|
246
|
+
</q-item-section>
|
|
247
|
+
</q-item>
|
|
248
|
+
|
|
249
|
+
<q-item clickable v-close-popup @click="copyMcpCommand(claudeCodeCommand, 'claude')" class="q-py-sm">
|
|
250
|
+
<q-item-section avatar>
|
|
251
|
+
<q-icon :name="claudeIcon" size="xs" />
|
|
252
|
+
</q-item-section>
|
|
253
|
+
<q-item-section>
|
|
254
|
+
<q-item-label>{{ copiedMcp === 'claude' ? t('page.copied') : t('page.connectClaudeCode') }}</q-item-label>
|
|
255
|
+
<q-item-label caption>{{ t('page.connectClaudeCodeCaption') }}</q-item-label>
|
|
256
|
+
</q-item-section>
|
|
257
|
+
<q-item-section side>
|
|
258
|
+
<q-icon :name="copiedMcp === 'claude' ? 'check' : 'content_copy'" size="xs" />
|
|
259
|
+
</q-item-section>
|
|
260
|
+
</q-item>
|
|
261
|
+
|
|
262
|
+
<q-item clickable v-close-popup @click="copyMcpCommand(codexCommand, 'codex')" class="q-py-sm">
|
|
263
|
+
<q-item-section avatar>
|
|
264
|
+
<q-icon :name="codexIcon" size="xs" />
|
|
265
|
+
</q-item-section>
|
|
266
|
+
<q-item-section>
|
|
267
|
+
<q-item-label>{{ copiedMcp === 'codex' ? t('page.copied') : t('page.connectCodex') }}</q-item-label>
|
|
268
|
+
<q-item-label caption>{{ t('page.connectCodexCaption') }}</q-item-label>
|
|
269
|
+
</q-item-section>
|
|
270
|
+
<q-item-section side>
|
|
271
|
+
<q-icon :name="copiedMcp === 'codex' ? 'check' : 'content_copy'" size="xs" />
|
|
272
|
+
</q-item-section>
|
|
273
|
+
</q-item>
|
|
274
|
+
</template>
|
|
166
275
|
</q-list>
|
|
167
276
|
</q-btn-dropdown>
|
|
168
277
|
</div>
|
package/src/i18n/helpers.js
CHANGED
|
@@ -33,7 +33,15 @@ 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',
|
|
39
|
+
connectVSCode: 'Connect to VSCode',
|
|
40
|
+
connectVSCodeCaption: 'Use this MCP in VSCode',
|
|
41
|
+
connectClaudeCode: 'Connect to Claude Code',
|
|
42
|
+
connectClaudeCodeCaption: 'Use this MCP in Claude Code',
|
|
43
|
+
connectCodex: 'Connect to Codex',
|
|
44
|
+
connectCodexCaption: 'Use this MCP in Codex'
|
|
37
45
|
}
|
|
38
46
|
},
|
|
39
47
|
'pt-BR': {
|
|
@@ -47,7 +55,15 @@ const engineDefaults = {
|
|
|
47
55
|
openInChatGPT: 'Abrir no ChatGPT',
|
|
48
56
|
openInChatGPTCaption: 'Pergunte ao ChatGPT sobre esta pΓ‘gina',
|
|
49
57
|
openInClaude: 'Abrir no Claude',
|
|
50
|
-
openInClaudeCaption: 'Pergunte ao Claude sobre esta pΓ‘gina'
|
|
58
|
+
openInClaudeCaption: 'Pergunte ao Claude sobre esta pΓ‘gina',
|
|
59
|
+
mcpServer: 'Servidor MCP',
|
|
60
|
+
mcpServerCaption: 'Conecte assistentes de IA via MCP',
|
|
61
|
+
connectVSCode: 'Conectar ao VSCode',
|
|
62
|
+
connectVSCodeCaption: 'Use este MCP no VSCode',
|
|
63
|
+
connectClaudeCode: 'Conectar ao Claude Code',
|
|
64
|
+
connectClaudeCodeCaption: 'Use este MCP no Claude Code',
|
|
65
|
+
connectCodex: 'Conectar ao Codex',
|
|
66
|
+
connectCodexCaption: 'Use este MCP no Codex'
|
|
51
67
|
}
|
|
52
68
|
}
|
|
53
69
|
}
|
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
|
}
|