@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 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.8.4'
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
@@ -35,6 +35,12 @@ export default {
35
35
  editBaseUrl: 'https://github.com/docsector/docsector-reader/edit/main/src/pages'
36
36
  },
37
37
 
38
+ // @ MCP
39
+ mcp: {
40
+ serverName: 'docsector-docs',
41
+ toolSuffix: 'docsector'
42
+ },
43
+
38
44
  // @ Languages
39
45
  languages: [
40
46
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docsector/docsector-reader",
3
- "version": "0.8.4",
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 (path, fillRule) {
21
+ function buildIconURI (paths, fillRule) {
21
22
  const fill = $q.dark.isActive ? '#ccc' : '#555'
22
23
  const fr = fillRule ? ` fill-rule="${fillRule}"` : ''
23
- const svg = `<svg xmlns="http://www.w3.org/2000/svg" fill="${fill}"${fr} viewBox="0 0 24 24"><path d="${path}"/></svg>`
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>
@@ -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
+ }
@@ -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
  }