@docsector/docsector-reader 1.4.0 → 1.6.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
@@ -30,6 +30,8 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
30
30
  - 🤖 **AI-Friendly robots.txt** — Scaffold includes a `robots.txt` explicitly allowing 23 AI crawlers (GPTBot, ClaudeBot, PerplexityBot, GrokBot, etc.)
31
31
  - 🧭 **Content Signals** — Optional `Content-Signal` directive for declaring AI usage policy (`ai-train`, `search`, `ai-input`) in `robots.txt`
32
32
  - 🧩 **Agent Skills Discovery Index** — Optional `/.well-known/agent-skills/index.json` with RFC v0.2.0 schema and SHA-256 digests
33
+ - 🪪 **MCP Server Card** — Optional `/.well-known/mcp/server-card.json` for MCP server discovery before connection
34
+ - 🌐 **WebMCP Browser Tools** — Optional registration of in-page tools via `navigator.modelContext` for browser agents
33
35
  - 🔗 **Homepage Link Headers** — Auto-generated `Link` response headers for agent discovery (`api-catalog`, `service-doc`, `service-desc`, `describedby`) per RFC 8288 / RFC 9727
34
36
  - 🔌 **MCP Server** — Auto-generated [MCP](https://modelcontextprotocol.io) server at `/mcp` for AI assistant integration (Claude Desktop, VS Code, etc.)
35
37
  - 📄 **llms.txt / llms-full.txt** — Auto-generated [llms.txt](https://llmstxt.org) index and full-content file for LLM discovery (requires `siteUrl` in config)
@@ -45,6 +47,7 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
45
47
  - 🌗 **Dark/Light Mode** — Automatic theme switching with Quasar Dark Plugin
46
48
  - 🔗 **Anchor Navigation** — Right-side Table of Contents tree with scroll tracking and auto-scroll to active section
47
49
  - 🔎 **Search** — Menu search across all documentation content and tags
50
+ - 🌐 **WebMCP Browser Tools** — Registers in-page tools for browser agents with `registerTool` and optional `provideContext` fallback
48
51
  - 📱 **Responsive** — Mobile-friendly with collapsible sidebar and drawers
49
52
  - 🏷️ **Status Badges** — Mark pages as `done`, `draft`, or `empty` with visual indicators
50
53
  - ✏️ **Edit on GitHub** — Direct links to edit pages on your repository
@@ -141,6 +144,126 @@ curl -X POST http://localhost:8788/mcp \
141
144
 
142
145
  ---
143
146
 
147
+ ## 🪪 MCP Server Card Discovery
148
+
149
+ Docsector Reader can publish an MCP Server Card at:
150
+
151
+ - `/.well-known/mcp/server-card.json`
152
+
153
+ This supports pre-connection MCP discovery, exposing:
154
+
155
+ - `serverInfo` (`name`, `version`)
156
+ - MCP transport endpoint (defaults to `/mcp`)
157
+ - `capabilities` for tools/resources/prompts
158
+
159
+ When MCP is enabled, tool capabilities are derived from the generated server:
160
+
161
+ - `search_{toolSuffix}`
162
+ - `get_page_{toolSuffix}`
163
+
164
+ ### Configure
165
+
166
+ ```javascript
167
+ export default {
168
+ // ...other config
169
+
170
+ mcp: {
171
+ serverName: 'my-docs',
172
+ toolSuffix: 'my_docs'
173
+ },
174
+
175
+ mcpServerCard: {
176
+ enabled: true,
177
+ path: '/.well-known/mcp/server-card.json',
178
+ transportEndpoint: '/mcp',
179
+ transportType: 'streamable-http',
180
+ protocolVersion: '2025-03-26',
181
+ capabilities: {
182
+ tools: { supported: true },
183
+ resources: { supported: false },
184
+ prompts: { supported: false }
185
+ }
186
+ }
187
+ }
188
+ ```
189
+
190
+ ### Validate
191
+
192
+ ```bash
193
+ npx docsector build
194
+ cat dist/spa/.well-known/mcp/server-card.json
195
+ cat dist/spa/_headers
196
+ ```
197
+
198
+ External validation:
199
+
200
+ ```bash
201
+ curl -X POST https://isitagentready.com/api/scan \
202
+ -H 'Content-Type: application/json' \
203
+ -d '{"url":"https://YOUR-SITE.com"}'
204
+ ```
205
+
206
+ Check `checks.discovery.mcpServerCard.status` equals `"pass"`.
207
+
208
+ ---
209
+
210
+ ## 🌐 WebMCP Browser Tools
211
+
212
+ Docsector Reader can register browser-side tools for agents when
213
+ `navigator.modelContext` is available (secure context required).
214
+
215
+ Default tools:
216
+
217
+ - `docs.search_docs` (bridges to MCP `search_{toolSuffix}`)
218
+ - `docs.get_page` (bridges to MCP `get_page_{toolSuffix}`)
219
+ - `docs.navigate_to` (SPA navigation)
220
+ - `docs.copy_current_page` (current page markdown URL/content)
221
+
222
+ ### WebMCP Configure
223
+
224
+ ```javascript
225
+ export default {
226
+ // ...other config
227
+
228
+ mcp: {
229
+ serverName: 'my-docs',
230
+ toolSuffix: 'my_docs'
231
+ },
232
+
233
+ webMcp: {
234
+ enabled: true,
235
+ apiMode: 'dual', // 'registerTool' | 'dual'
236
+ toolPrefix: 'docs',
237
+ bridgeEndpoint: '/mcp',
238
+ bridgeToMcp: true,
239
+ tools: {
240
+ searchDocs: true,
241
+ getPage: true,
242
+ navigateTo: true,
243
+ copyCurrentPage: true
244
+ }
245
+ }
246
+ }
247
+ ```
248
+
249
+ Notes:
250
+
251
+ - `apiMode: 'registerTool'` uses only `navigator.modelContext.registerTool()`.
252
+ - `apiMode: 'dual'` also attempts `provideContext` fallback when available.
253
+ - Registration happens on page load and is automatically cleaned up on unmount.
254
+
255
+ ### WebMCP Validate
256
+
257
+ ```bash
258
+ curl -X POST https://isitagentready.com/api/scan \
259
+ -H 'Content-Type: application/json' \
260
+ -d '{"url":"https://YOUR-SITE.com"}'
261
+ ```
262
+
263
+ Check `checks.discovery.webMcp.status` equals `"pass"`.
264
+
265
+ ---
266
+
144
267
  ## � llms.txt (LLM Discovery)
145
268
 
146
269
  Docsector Reader automatically generates [llms.txt](https://llmstxt.org) files at build time when `siteUrl` is configured (same requirement as sitemap.xml).
@@ -550,6 +673,14 @@ export default {
550
673
  agentFallback: true
551
674
  },
552
675
 
676
+ mcpServerCard: {
677
+ enabled: true,
678
+ path: '/.well-known/mcp/server-card.json',
679
+ transportEndpoint: '/mcp',
680
+ transportType: 'streamable-http',
681
+ protocolVersion: '2025-03-26'
682
+ },
683
+
553
684
  contentSignals: {
554
685
  enabled: true,
555
686
  aiTrain: 'yes',
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 = '1.4.0'
26
+ const VERSION = '1.6.0'
27
27
 
28
28
  const HELP = `
29
29
  Docsector Reader v${VERSION}
@@ -133,6 +133,38 @@ export default {
133
133
  // toolSuffix: 'my_docs'
134
134
  // },
135
135
 
136
+ // @ MCP Server Card discovery (optional)
137
+ // Publishes /.well-known/mcp/server-card.json for pre-connection discovery.
138
+ // mcpServerCard: {
139
+ // enabled: true,
140
+ // path: '/.well-known/mcp/server-card.json',
141
+ // transportEndpoint: '/mcp',
142
+ // transportType: 'streamable-http',
143
+ // protocolVersion: '2025-03-26',
144
+ // capabilities: {
145
+ // tools: { supported: true },
146
+ // resources: { supported: false },
147
+ // prompts: { supported: false }
148
+ // }
149
+ // },
150
+
151
+ // @ WebMCP browser tools (optional)
152
+ // Registers tools in browser contexts that expose navigator.modelContext.
153
+ // Uses registerTool when available, with optional provideContext fallback.
154
+ // webMcp: {
155
+ // enabled: true,
156
+ // apiMode: 'dual', // 'registerTool' | 'dual'
157
+ // toolPrefix: 'docs',
158
+ // bridgeEndpoint: '/mcp',
159
+ // bridgeToMcp: true,
160
+ // tools: {
161
+ // searchDocs: true,
162
+ // getPage: true,
163
+ // navigateTo: true,
164
+ // copyCurrentPage: true
165
+ // }
166
+ // },
167
+
136
168
  // @ Homepage Link headers for agent discovery (optional)
137
169
  // linkHeaders: {
138
170
  // enabled: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docsector/docsector-reader",
3
- "version": "1.4.0",
3
+ "version": "1.6.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",
package/src/App.vue CHANGED
@@ -114,20 +114,24 @@
114
114
  </template>
115
115
 
116
116
  <script setup>
117
- import { reactive, computed, onMounted } from 'vue'
117
+ import { reactive, computed, onMounted, onBeforeUnmount } from 'vue'
118
118
  import { useQuasar } from 'quasar'
119
119
  import { useStore } from 'vuex'
120
120
  import { useI18n } from 'vue-i18n'
121
- import { useRouter } from 'vue-router'
121
+ import { useRouter, useRoute } from 'vue-router'
122
122
 
123
123
  import docsectorConfig from 'docsector.config.js'
124
+ import { setupWebMcp } from './composables/useWebMcp'
124
125
 
125
126
  defineOptions({ name: 'App' })
126
127
 
127
128
  const $q = useQuasar()
128
129
  const store = useStore()
129
- const { locale } = useI18n()
130
+ const { locale, t } = useI18n()
130
131
  const router = useRouter()
132
+ const route = useRoute()
133
+
134
+ let cleanupWebMcp = null
131
135
 
132
136
  const settings = reactive({
133
137
  general: {
@@ -213,6 +217,21 @@ onMounted(() => {
213
217
  settings.appearance.background.default = dark
214
218
  }
215
219
  $q.dark.set(dark)
220
+
221
+ cleanupWebMcp = setupWebMcp({
222
+ router,
223
+ route,
224
+ store,
225
+ translate: t,
226
+ locale
227
+ })
228
+ })
229
+
230
+ onBeforeUnmount(() => {
231
+ if (typeof cleanupWebMcp === 'function') {
232
+ cleanupWebMcp()
233
+ cleanupWebMcp = null
234
+ }
216
235
  })
217
236
  </script>
218
237
 
@@ -0,0 +1,398 @@
1
+ import docsectorConfig from 'docsector.config.js'
2
+
3
+ let activeCleanup = null
4
+
5
+ function toSafeToolPrefix (prefix) {
6
+ const value = String(prefix || 'docs')
7
+ .trim()
8
+ .replace(/[^A-Za-z0-9_.-]/g, '_')
9
+
10
+ if (!value) {
11
+ return 'docs'
12
+ }
13
+
14
+ return value.slice(0, 48)
15
+ }
16
+
17
+ function decodeMarkdownSource (source) {
18
+ return String(source || '')
19
+ .replace(/&#123;/g, '{')
20
+ .replace(/&#125;/g, '}')
21
+ .replace(/\{'([^']+)'\}/g, '$1')
22
+ .replace(/&amp;/g, '&')
23
+ }
24
+
25
+ function isSecurePageContext () {
26
+ if (typeof window === 'undefined') return false
27
+
28
+ if (window.isSecureContext) return true
29
+
30
+ const protocol = window.location?.protocol || ''
31
+ const host = window.location?.hostname || ''
32
+ return protocol === 'https:' || host === 'localhost' || host === '127.0.0.1'
33
+ }
34
+
35
+ function buildMcpEndpoint (bridgeEndpoint) {
36
+ if (typeof window === 'undefined') return '/mcp'
37
+
38
+ const endpoint = bridgeEndpoint || '/mcp'
39
+ try {
40
+ return new URL(endpoint, window.location.origin).toString()
41
+ } catch {
42
+ return `${window.location.origin}/mcp`
43
+ }
44
+ }
45
+
46
+ async function callMcpTool ({ endpoint, toolName, args }) {
47
+ const requestId = `${toolName}:${Date.now()}`
48
+ const response = await fetch(endpoint, {
49
+ method: 'POST',
50
+ headers: { 'Content-Type': 'application/json' },
51
+ body: JSON.stringify({
52
+ jsonrpc: '2.0',
53
+ id: requestId,
54
+ method: 'tools/call',
55
+ params: {
56
+ name: toolName,
57
+ arguments: args || {}
58
+ }
59
+ })
60
+ })
61
+
62
+ if (!response.ok) {
63
+ throw new Error(`MCP endpoint responded with ${response.status}`)
64
+ }
65
+
66
+ const payload = await response.json()
67
+ if (payload.error) {
68
+ throw new Error(payload.error.message || 'MCP tool call failed')
69
+ }
70
+
71
+ return payload.result || null
72
+ }
73
+
74
+ function normalizePath (inputPath) {
75
+ const value = String(inputPath || '').trim()
76
+ if (!value) return '/'
77
+
78
+ if (value.startsWith('http://') || value.startsWith('https://')) {
79
+ try {
80
+ const url = new URL(value)
81
+ return `${url.pathname}${url.search}${url.hash}`
82
+ } catch {
83
+ return '/'
84
+ }
85
+ }
86
+
87
+ if (value.startsWith('/')) {
88
+ return value
89
+ }
90
+
91
+ return `/${value}`
92
+ }
93
+
94
+ function createToolDefinitions ({
95
+ prefix,
96
+ mcpToolSuffix,
97
+ bridgeEndpoint,
98
+ bridgeToMcp,
99
+ tools,
100
+ router,
101
+ getCurrentPath,
102
+ getCurrentHash,
103
+ getLocale,
104
+ getAbsoluteI18nPath,
105
+ translate
106
+ }) {
107
+ const endpoint = buildMcpEndpoint(bridgeEndpoint)
108
+
109
+ const maybeCallMcp = async (toolName, args) => {
110
+ if (!bridgeToMcp) {
111
+ throw new Error('MCP bridge disabled in webMcp configuration')
112
+ }
113
+
114
+ return callMcpTool({ endpoint, toolName, args })
115
+ }
116
+
117
+ const definitions = []
118
+
119
+ if (tools.searchDocs) {
120
+ definitions.push({
121
+ name: `${prefix}.search_docs`,
122
+ description: 'Search documentation pages by keyword and return top matches.',
123
+ inputSchema: {
124
+ type: 'object',
125
+ properties: {
126
+ query: {
127
+ type: 'string',
128
+ description: 'Search term or phrase.'
129
+ }
130
+ },
131
+ required: ['query']
132
+ },
133
+ annotations: { readOnlyHint: true },
134
+ execute: async (input) => {
135
+ const query = String(input?.query || '').trim()
136
+ if (!query) {
137
+ return {
138
+ ok: false,
139
+ error: 'Missing required field: query'
140
+ }
141
+ }
142
+
143
+ const toolName = `search_${mcpToolSuffix}`
144
+ const result = await maybeCallMcp(toolName, { query })
145
+
146
+ return {
147
+ ok: true,
148
+ query,
149
+ mcpTool: toolName,
150
+ endpoint,
151
+ result
152
+ }
153
+ }
154
+ })
155
+ }
156
+
157
+ if (tools.getPage) {
158
+ definitions.push({
159
+ name: `${prefix}.get_page`,
160
+ description: 'Fetch a documentation page in markdown format by its path.',
161
+ inputSchema: {
162
+ type: 'object',
163
+ properties: {
164
+ path: {
165
+ type: 'string',
166
+ description: 'Documentation page path without trailing .md.'
167
+ }
168
+ },
169
+ required: ['path']
170
+ },
171
+ annotations: { readOnlyHint: true },
172
+ execute: async (input) => {
173
+ const path = String(input?.path || '').trim()
174
+ if (!path) {
175
+ return {
176
+ ok: false,
177
+ error: 'Missing required field: path'
178
+ }
179
+ }
180
+
181
+ const toolName = `get_page_${mcpToolSuffix}`
182
+ const result = await maybeCallMcp(toolName, { path })
183
+
184
+ return {
185
+ ok: true,
186
+ path,
187
+ mcpTool: toolName,
188
+ endpoint,
189
+ result
190
+ }
191
+ }
192
+ })
193
+ }
194
+
195
+ if (tools.navigateTo) {
196
+ definitions.push({
197
+ name: `${prefix}.navigate_to`,
198
+ description: 'Navigate to a docs route and optional anchor hash in the current session.',
199
+ inputSchema: {
200
+ type: 'object',
201
+ properties: {
202
+ path: {
203
+ type: 'string',
204
+ description: 'Target path (absolute or relative).'
205
+ },
206
+ hash: {
207
+ type: 'string',
208
+ description: 'Optional hash fragment, with or without leading #.'
209
+ }
210
+ }
211
+ },
212
+ annotations: { readOnlyHint: false },
213
+ execute: async (input) => {
214
+ const path = normalizePath(input?.path || getCurrentPath())
215
+ const hashInput = String(input?.hash || '').trim()
216
+ const hash = hashInput ? (hashInput.startsWith('#') ? hashInput : `#${hashInput}`) : ''
217
+
218
+ await router.push({ path, hash })
219
+
220
+ return {
221
+ ok: true,
222
+ path,
223
+ hash,
224
+ currentPath: getCurrentPath(),
225
+ currentHash: getCurrentHash()
226
+ }
227
+ }
228
+ })
229
+ }
230
+
231
+ if (tools.copyCurrentPage) {
232
+ definitions.push({
233
+ name: `${prefix}.copy_current_page`,
234
+ description: 'Return markdown URL and markdown source for the current page context.',
235
+ inputSchema: {
236
+ type: 'object',
237
+ properties: {
238
+ includeContent: {
239
+ type: 'boolean',
240
+ description: 'When false, return metadata and URL only.'
241
+ }
242
+ }
243
+ },
244
+ annotations: { readOnlyHint: true },
245
+ execute: async (input) => {
246
+ const locale = getLocale() || 'en-US'
247
+ const currentPath = getCurrentPath()
248
+ const path = currentPath === '/' ? '/Homepage' : currentPath.replace(/\/+$/, '')
249
+ const markdownUrl = `${window.location.origin}${path}.md`
250
+
251
+ const includeContent = input?.includeContent !== false
252
+ const absolute = getAbsoluteI18nPath()
253
+
254
+ let content = ''
255
+ if (includeContent && absolute) {
256
+ const source = translate(`_.${absolute}.source`)
257
+ if (source) {
258
+ content = decodeMarkdownSource(source)
259
+ }
260
+ }
261
+
262
+ return {
263
+ ok: true,
264
+ locale,
265
+ path: currentPath,
266
+ markdownUrl,
267
+ content
268
+ }
269
+ }
270
+ })
271
+ }
272
+
273
+ return definitions
274
+ }
275
+
276
+ function registerWithProvideContext (modelContext, toolDefinitions) {
277
+ if (typeof modelContext.provideContext !== 'function') return null
278
+
279
+ const payload = {
280
+ tools: toolDefinitions.map((tool) => ({
281
+ name: tool.name,
282
+ description: tool.description,
283
+ inputSchema: tool.inputSchema,
284
+ execute: tool.execute
285
+ }))
286
+ }
287
+
288
+ const result = modelContext.provideContext(payload)
289
+
290
+ if (typeof result === 'function') {
291
+ return result
292
+ }
293
+
294
+ if (result && typeof result.dispose === 'function') {
295
+ return () => result.dispose()
296
+ }
297
+
298
+ if (result && typeof result.unregister === 'function') {
299
+ return () => result.unregister()
300
+ }
301
+
302
+ return null
303
+ }
304
+
305
+ export function setupWebMcp ({ router, route, store, translate, locale }) {
306
+ const webMcp = docsectorConfig.webMcp || {}
307
+ if (!webMcp.enabled) return () => {}
308
+
309
+ if (!isSecurePageContext()) return () => {}
310
+
311
+ const modelContext = navigator?.modelContext
312
+ if (!modelContext) return () => {}
313
+
314
+ if (activeCleanup) {
315
+ activeCleanup()
316
+ activeCleanup = null
317
+ }
318
+
319
+ const prefix = toSafeToolPrefix(webMcp.toolPrefix || 'docs')
320
+ const mcpToolSuffix = String(docsectorConfig.mcp?.toolSuffix || 'docs')
321
+ .replace(/[^A-Za-z0-9_]/g, '_')
322
+
323
+ const tools = {
324
+ searchDocs: webMcp.tools?.searchDocs !== false,
325
+ getPage: webMcp.tools?.getPage !== false,
326
+ navigateTo: webMcp.tools?.navigateTo !== false,
327
+ copyCurrentPage: webMcp.tools?.copyCurrentPage !== false
328
+ }
329
+
330
+ const definitions = createToolDefinitions({
331
+ prefix,
332
+ mcpToolSuffix,
333
+ bridgeEndpoint: webMcp.bridgeEndpoint || '/mcp',
334
+ bridgeToMcp: webMcp.bridgeToMcp !== false,
335
+ tools,
336
+ router,
337
+ getCurrentPath: () => route.path,
338
+ getCurrentHash: () => route.hash,
339
+ getLocale: () => locale.value,
340
+ getAbsoluteI18nPath: () => store?.state?.i18n?.absolute,
341
+ translate
342
+ })
343
+
344
+ if (definitions.length === 0) return () => {}
345
+
346
+ const supportsRegisterTool = typeof modelContext.registerTool === 'function'
347
+ const supportsProvideContext = typeof modelContext.provideContext === 'function'
348
+ const mode = webMcp.apiMode === 'registerTool' ? 'registerTool' : 'dual'
349
+
350
+ const abortController = new AbortController()
351
+ const cleanups = [() => abortController.abort()]
352
+
353
+ try {
354
+ if (supportsRegisterTool) {
355
+ for (const tool of definitions) {
356
+ modelContext.registerTool({
357
+ name: tool.name,
358
+ description: tool.description,
359
+ inputSchema: tool.inputSchema,
360
+ annotations: tool.annotations,
361
+ execute: tool.execute
362
+ }, { signal: abortController.signal })
363
+ }
364
+ } else if (mode === 'registerTool') {
365
+ return () => {}
366
+ }
367
+
368
+ if ((!supportsRegisterTool || mode === 'dual') && supportsProvideContext) {
369
+ const provideCleanup = registerWithProvideContext(modelContext, definitions)
370
+ if (provideCleanup) {
371
+ cleanups.push(provideCleanup)
372
+ }
373
+ }
374
+ } catch (error) {
375
+ if (import.meta.env?.DEV) {
376
+ console.warn('[docsector:webmcp] Failed to register tools', error)
377
+ }
378
+ return () => {}
379
+ }
380
+
381
+ const cleanup = () => {
382
+ while (cleanups.length > 0) {
383
+ const fn = cleanups.pop()
384
+ try {
385
+ fn()
386
+ } catch {
387
+ // Ignore cleanup errors
388
+ }
389
+ }
390
+ }
391
+
392
+ activeCleanup = cleanup
393
+ return cleanup
394
+ }
395
+
396
+ export default {
397
+ setupWebMcp
398
+ }
package/src/index.js CHANGED
@@ -78,6 +78,26 @@
78
78
  * @param {string} [config.agentSkills.path='/.well-known/agent-skills/index.json'] - Output URI path for Agent Skills index
79
79
  * @param {string} [config.agentSkills.schema='https://schemas.agentskills.io/discovery/0.2.0/schema.json'] - JSON Schema identifier for index payload
80
80
  * @param {Array<{name:string,type:'skill-md'|'archive',description:string,url:string,digest?:string}>} [config.agentSkills.skills=[]] - Skills to publish in discovery index
81
+ * @param {Object} [config.mcpServerCard] - MCP Server Card discovery settings
82
+ * @param {boolean} [config.mcpServerCard.enabled=false] - Enables generation of MCP Server Card discovery document
83
+ * @param {string} [config.mcpServerCard.path='/.well-known/mcp/server-card.json'] - Output URI path for MCP Server Card
84
+ * @param {string} [config.mcpServerCard.transportEndpoint='/mcp'] - MCP transport endpoint exposed by the server
85
+ * @param {string} [config.mcpServerCard.transportType='streamable-http'] - Transport type label for discovery metadata
86
+ * @param {string} [config.mcpServerCard.protocolVersion='2025-03-26'] - Protocol version advertised by the server card
87
+ * @param {Object} [config.mcpServerCard.capabilities] - Optional capability overrides for tools/resources/prompts
88
+ * @param {Array<Object>} [config.mcpServerCard.remotes=[]] - Optional additional remotes to include in the server card
89
+ * @param {Object} [config.mcpServerCard.metadata] - Optional additional metadata merged into the server card payload
90
+ * @param {Object} [config.webMcp] - WebMCP browser tools settings
91
+ * @param {boolean} [config.webMcp.enabled=false] - Enables browser-side WebMCP tool registration on page load
92
+ * @param {'registerTool'|'dual'} [config.webMcp.apiMode='dual'] - Registration mode: registerTool only or registerTool + provideContext fallback
93
+ * @param {string} [config.webMcp.toolPrefix='docs'] - Prefix used to build WebMCP tool names (e.g. docs.search_docs)
94
+ * @param {string} [config.webMcp.bridgeEndpoint='/mcp'] - Relative endpoint used to bridge search/get_page to the MCP HTTP server
95
+ * @param {boolean} [config.webMcp.bridgeToMcp=true] - Uses MCP endpoint bridge for search/get_page tools when true
96
+ * @param {Object} [config.webMcp.tools] - Per-tool enable flags
97
+ * @param {boolean} [config.webMcp.tools.searchDocs=true] - Enables tool search_docs
98
+ * @param {boolean} [config.webMcp.tools.getPage=true] - Enables tool get_page
99
+ * @param {boolean} [config.webMcp.tools.navigateTo=true] - Enables tool navigate_to
100
+ * @param {boolean} [config.webMcp.tools.copyCurrentPage=true] - Enables tool copy_current_page
81
101
  * @returns {Object} Resolved Docsector configuration
82
102
  */
83
103
  export function createDocsector (config = {}) {
@@ -169,6 +189,34 @@ export function createDocsector (config = {}) {
169
189
  schema: 'https://schemas.agentskills.io/discovery/0.2.0/schema.json',
170
190
  skills: [],
171
191
  ...config.agentSkills
192
+ },
193
+
194
+ mcpServerCard: {
195
+ enabled: false,
196
+ path: '/.well-known/mcp/server-card.json',
197
+ transportEndpoint: '/mcp',
198
+ transportType: 'streamable-http',
199
+ protocolVersion: '2025-03-26',
200
+ capabilities: null,
201
+ remotes: [],
202
+ metadata: null,
203
+ ...config.mcpServerCard
204
+ },
205
+
206
+ webMcp: {
207
+ enabled: false,
208
+ apiMode: 'dual',
209
+ toolPrefix: 'docs',
210
+ bridgeEndpoint: '/mcp',
211
+ bridgeToMcp: true,
212
+ ...config.webMcp,
213
+ tools: {
214
+ searchDocs: true,
215
+ getPage: true,
216
+ navigateTo: true,
217
+ copyCurrentPage: true,
218
+ ...(config.webMcp?.tools || {})
219
+ }
172
220
  }
173
221
  }
174
222
  }
@@ -757,6 +757,8 @@ function createMarkdownBuildPlugin (projectRoot) {
757
757
  const contentSignalsEnabled = contentSignalsConfig.enabled === true
758
758
  const agentSkillsConfig = config.agentSkills || {}
759
759
  const agentSkillsEnabled = agentSkillsConfig.enabled === true
760
+ const mcpServerCardConfig = config.mcpServerCard || {}
761
+ const mcpServerCardEnabled = mcpServerCardConfig.enabled === true
760
762
 
761
763
  const toUrl = (href) => {
762
764
  if (!href) return null
@@ -1112,6 +1114,54 @@ export async function onRequest (context) {
1112
1114
  }
1113
1115
  }
1114
1116
 
1117
+ if (mcpServerCardEnabled) {
1118
+ if (!config.mcp) {
1119
+ console.warn('\x1b[33m[docsector]\x1b[0m Skipped MCP Server Card generation: mcp config is not enabled')
1120
+ } else {
1121
+ const mcpServerCardPath = mcpServerCardConfig.path || '/.well-known/mcp/server-card.json'
1122
+ const cardDistPath = normalizeLocalPath(mcpServerCardPath)
1123
+
1124
+ if (!cardDistPath) {
1125
+ console.warn(`\x1b[33m[docsector]\x1b[0m Skipped MCP Server Card generation: path must be a local URI path, got "${mcpServerCardPath}"`)
1126
+ } else {
1127
+ const cardHref = mcpServerCardPath.startsWith('/') ? mcpServerCardPath : `/${mcpServerCardPath}`
1128
+ const mcpServerName = config.mcp.serverName || config.branding?.name || 'docs'
1129
+ const mcpServerVersion = config.branding?.version || '1.0.0'
1130
+ const mcpToolSuffix = config.mcp.toolSuffix || 'docs'
1131
+ const transportEndpoint = mcpServerCardConfig.transportEndpoint || '/mcp'
1132
+ const endpoint = toUrl(transportEndpoint)
1133
+
1134
+ if (!endpoint) {
1135
+ console.warn('\x1b[33m[docsector]\x1b[0m Skipped MCP Server Card generation: unable to resolve transport endpoint URL')
1136
+ } else {
1137
+ const cardPayload = buildMcpServerCardPayload({
1138
+ config,
1139
+ mcpServerCardConfig,
1140
+ serverName: mcpServerName,
1141
+ serverVersion: mcpServerVersion,
1142
+ endpoint,
1143
+ toolSuffix: mcpToolSuffix
1144
+ })
1145
+
1146
+ const cardDir = resolve(distDir, cardDistPath, '..')
1147
+ mkdirSync(cardDir, { recursive: true })
1148
+ writeFileSync(
1149
+ resolve(distDir, cardDistPath),
1150
+ JSON.stringify(cardPayload, null, 2) + '\n'
1151
+ )
1152
+ console.log(`\x1b[36m[docsector]\x1b[0m Generated ${cardHref}`)
1153
+
1154
+ const headersWithServerCard = readFileSync(headersPath, 'utf-8')
1155
+ if (!headersWithServerCard.includes(cardHref)) {
1156
+ const serverCardHeaders = `${cardHref}\n Content-Type: application/json; charset=utf-8\n`
1157
+ writeFileSync(headersPath, headersWithServerCard.trimEnd() + '\n\n' + serverCardHeaders)
1158
+ console.log(`\x1b[36m[docsector]\x1b[0m Added _headers rule for ${cardHref}`)
1159
+ }
1160
+ }
1161
+ }
1162
+ }
1163
+ }
1164
+
1115
1165
  console.log(`\x1b[36m[docsector]\x1b[0m Added _headers rule for .md files`)
1116
1166
 
1117
1167
  // Add homepage Link headers for agent discovery (RFC 8288 / RFC 9727)
@@ -1504,6 +1554,87 @@ function resolveAgentSkillArtifactPath (artifactUrl, { siteUrl, distDir }) {
1504
1554
  return resolve(distDir, relativePath)
1505
1555
  }
1506
1556
 
1557
+ function mergeObjects (base, override) {
1558
+ if (!override || typeof override !== 'object' || Array.isArray(override)) {
1559
+ return base
1560
+ }
1561
+
1562
+ return { ...base, ...override }
1563
+ }
1564
+
1565
+ function buildMcpServerCardPayload ({
1566
+ config,
1567
+ mcpServerCardConfig,
1568
+ serverName,
1569
+ serverVersion,
1570
+ endpoint,
1571
+ toolSuffix
1572
+ }) {
1573
+ const transportType = mcpServerCardConfig.transportType || 'streamable-http'
1574
+ const protocolVersion = mcpServerCardConfig.protocolVersion || '2025-03-26'
1575
+ const baseTools = [
1576
+ `search_${toolSuffix}`,
1577
+ `get_page_${toolSuffix}`
1578
+ ]
1579
+
1580
+ const defaultCapabilities = {
1581
+ tools: {
1582
+ supported: true,
1583
+ names: baseTools
1584
+ },
1585
+ resources: {
1586
+ supported: false
1587
+ },
1588
+ prompts: {
1589
+ supported: false
1590
+ }
1591
+ }
1592
+
1593
+ const capabilitiesOverride = (mcpServerCardConfig.capabilities && typeof mcpServerCardConfig.capabilities === 'object' && !Array.isArray(mcpServerCardConfig.capabilities))
1594
+ ? mcpServerCardConfig.capabilities
1595
+ : {}
1596
+ const capabilities = {
1597
+ ...capabilitiesOverride,
1598
+ tools: mergeObjects(defaultCapabilities.tools, capabilitiesOverride.tools),
1599
+ resources: mergeObjects(defaultCapabilities.resources, capabilitiesOverride.resources),
1600
+ prompts: mergeObjects(defaultCapabilities.prompts, capabilitiesOverride.prompts)
1601
+ }
1602
+
1603
+ const primaryRemote = {
1604
+ transportType,
1605
+ endpoint,
1606
+ supportedProtocolVersions: [protocolVersion]
1607
+ }
1608
+
1609
+ const customRemotes = Array.isArray(mcpServerCardConfig.remotes)
1610
+ ? mcpServerCardConfig.remotes
1611
+ : []
1612
+ const remotes = [primaryRemote, ...customRemotes]
1613
+
1614
+ const payload = {
1615
+ serverInfo: {
1616
+ name: serverName,
1617
+ version: serverVersion
1618
+ },
1619
+ title: config.branding?.name || serverName,
1620
+ description: config.branding?.description || null,
1621
+ transport: {
1622
+ type: transportType,
1623
+ endpoint
1624
+ },
1625
+ remotes,
1626
+ capabilities,
1627
+ tools: baseTools.map(name => ({ name }))
1628
+ }
1629
+
1630
+ const metadata = mcpServerCardConfig.metadata
1631
+ if (metadata && typeof metadata === 'object' && !Array.isArray(metadata)) {
1632
+ return { ...payload, ...metadata }
1633
+ }
1634
+
1635
+ return payload
1636
+ }
1637
+
1507
1638
  /**
1508
1639
  * Create a complete Quasar configuration for a docsector-reader consumer project.
1509
1640
  *