@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 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.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
@@ -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.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 (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,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>
@@ -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
+ }
@@ -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
  }